Merge pull request #39808 from nextcloud/feat/f2v/files
120
.drone.yml
|
|
@ -1591,126 +1591,6 @@ trigger:
|
|||
- pull_request
|
||||
- push
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
name: acceptance-app-files
|
||||
|
||||
steps:
|
||||
- name: submodules
|
||||
image: ghcr.io/nextcloud/continuous-integration-alpine-git:latest
|
||||
commands:
|
||||
- git submodule update --init
|
||||
- name: acceptance-app-files
|
||||
image: ghcr.io/nextcloud/continuous-integration-acceptance-php8.0:latest
|
||||
commands:
|
||||
- tests/acceptance/run-local.sh --timeout-multiplier 10 --nextcloud-server-domain acceptance-app-files --selenium-server selenium:4444 allow-git-repository-modifications features/app-files.feature
|
||||
|
||||
services:
|
||||
- name: selenium
|
||||
image: ghcr.io/nextcloud/continuous-integration-selenium:3.141.59
|
||||
environment:
|
||||
# Reduce default log level for Selenium server (INFO) as it is too
|
||||
# verbose.
|
||||
JAVA_OPTS: -Dselenium.LOGGER.level=WARNING
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
- master
|
||||
- stable*
|
||||
event:
|
||||
- pull_request
|
||||
- push
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
name: acceptance-app-files-sharing
|
||||
|
||||
steps:
|
||||
- name: submodules
|
||||
image: ghcr.io/nextcloud/continuous-integration-alpine-git:latest
|
||||
commands:
|
||||
- git submodule update --init
|
||||
- name: acceptance-app-files-sharing
|
||||
image: ghcr.io/nextcloud/continuous-integration-acceptance-php8.0:latest
|
||||
commands:
|
||||
- tests/acceptance/run-local.sh --timeout-multiplier 10 --nextcloud-server-domain acceptance-app-files-sharing --selenium-server selenium:4444 allow-git-repository-modifications features/app-files-sharing.feature
|
||||
|
||||
services:
|
||||
- name: selenium
|
||||
image: ghcr.io/nextcloud/continuous-integration-selenium:3.141.59
|
||||
environment:
|
||||
# Reduce default log level for Selenium server (INFO) as it is too
|
||||
# verbose.
|
||||
JAVA_OPTS: -Dselenium.LOGGER.level=WARNING
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
- master
|
||||
- stable*
|
||||
event:
|
||||
- pull_request
|
||||
- push
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
name: acceptance-app-files-sharing-link
|
||||
|
||||
steps:
|
||||
- name: submodules
|
||||
image: ghcr.io/nextcloud/continuous-integration-alpine-git:latest
|
||||
commands:
|
||||
- git submodule update --init
|
||||
- name: acceptance-app-files-sharing-link
|
||||
image: ghcr.io/nextcloud/continuous-integration-acceptance-php8.0:latest
|
||||
commands:
|
||||
- tests/acceptance/run-local.sh --timeout-multiplier 10 --nextcloud-server-domain acceptance-app-files-sharing-link --selenium-server selenium:4444 allow-git-repository-modifications features/app-files-sharing-link.feature
|
||||
|
||||
services:
|
||||
- name: selenium
|
||||
image: ghcr.io/nextcloud/continuous-integration-selenium:3.141.59
|
||||
environment:
|
||||
# Reduce default log level for Selenium server (INFO) as it is too
|
||||
# verbose.
|
||||
JAVA_OPTS: -Dselenium.LOGGER.level=WARNING
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
- master
|
||||
- stable*
|
||||
event:
|
||||
- pull_request
|
||||
- push
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
name: acceptance-app-files-tags
|
||||
|
||||
steps:
|
||||
- name: submodules
|
||||
image: ghcr.io/nextcloud/continuous-integration-alpine-git:latest
|
||||
commands:
|
||||
- git submodule update --init
|
||||
- name: acceptance-app-files-tags
|
||||
image: ghcr.io/nextcloud/continuous-integration-acceptance-php8.0:latest
|
||||
commands:
|
||||
- tests/acceptance/run-local.sh --timeout-multiplier 10 --nextcloud-server-domain acceptance-app-files-tags --selenium-server selenium:4444 allow-git-repository-modifications features/app-files-tags.feature
|
||||
|
||||
services:
|
||||
- name: selenium
|
||||
image: ghcr.io/nextcloud/continuous-integration-selenium:3.141.59
|
||||
environment:
|
||||
# Reduce default log level for Selenium server (INFO) as it is too
|
||||
# verbose.
|
||||
JAVA_OPTS: -Dselenium.LOGGER.level=WARNING
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
- master
|
||||
- stable*
|
||||
event:
|
||||
- pull_request
|
||||
- push
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
name: acceptance-header
|
||||
|
|
|
|||
|
|
@ -24,3 +24,6 @@ import '@testing-library/jest-dom'
|
|||
|
||||
// Mock `window.location` with Jest spies and extend expect
|
||||
import 'jest-location-mock'
|
||||
|
||||
// Mock `window.fetch` with Jest
|
||||
import 'jest-fetch-mock'
|
||||
|
|
|
|||
|
|
@ -139,16 +139,14 @@ $application->registerRoutes(
|
|||
'verb' => 'GET'
|
||||
],
|
||||
[
|
||||
'name' => 'view#index',
|
||||
'name' => 'view#indexView',
|
||||
'url' => '/{view}',
|
||||
'verb' => 'GET',
|
||||
'postfix' => 'view',
|
||||
],
|
||||
[
|
||||
'name' => 'view#index',
|
||||
'name' => 'view#indexViewFileid',
|
||||
'url' => '/{view}/{fileid}',
|
||||
'verb' => 'GET',
|
||||
'postfix' => 'fileid',
|
||||
],
|
||||
],
|
||||
'ocs' => [
|
||||
|
|
|
|||
|
|
@ -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,iBACA,YACA,SACA,oDACA,8CACA,aACA,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,gBC9EC,yCDiFD,iBCjFC,yCDoFD,oBCpFC,0CDuFD,qGCvFC,wCD6FD,0BC7FC,yCDgGD,2BChGC,2CDmGD,mBCnGC,yCDsGD,2BCtGC,4CDyGD,2BCzGC,0CD4GD,4BC5GC,4CDgHD,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,2BAKA,0EACC,wCAKF,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,kBACA,YACA,SACA,oDACA,8CACA,aACA,iBAGD,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"],"names":[],"mappings":"AAWA,SAEC,YACA,YACA,qBACA,WAED,oEACA,8BACA,kDAEC,+CAED,0BACC,oDAGD,mBACC,kBACA,aACA,SACA,4CACC,iBACA,YACA,SACA,oDACA,8CACA,aACA,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,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,2BAKA,0EACC,wCAKF,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,kBACA,YACA,SACA,oDACA,8CACA,aACA,iBAGD,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"}
|
||||
|
|
@ -140,44 +140,6 @@
|
|||
background-color: var(--color-primary-element-light) !important;
|
||||
}
|
||||
|
||||
/* icons for sidebar */
|
||||
.nav-icon-files {
|
||||
@include icon-color('folder', 'files', variables.$color-black);
|
||||
}
|
||||
.nav-icon-recent {
|
||||
@include icon-color('recent', 'files', variables.$color-black);
|
||||
}
|
||||
.nav-icon-favorites {
|
||||
@include icon-color('starred', 'actions', variables.$color-black, 2, true);
|
||||
}
|
||||
.nav-icon-sharinginOld,
|
||||
.nav-icon-sharingoutOld,
|
||||
.nav-icon-pendingsharesOld,
|
||||
.nav-icon-shareoverviewOld {
|
||||
@include icon-color('share', 'files', variables.$color-black);
|
||||
}
|
||||
.nav-icon-sharinglinksOld {
|
||||
@include icon-color('public', 'files', variables.$color-black);
|
||||
}
|
||||
.nav-icon-extstoragemounts {
|
||||
@include icon-color('external', 'files', variables.$color-black);
|
||||
}
|
||||
.nav-icon-trashbin {
|
||||
@include icon-color('delete', 'files', variables.$color-black);
|
||||
}
|
||||
.nav-icon-trashbin-starred {
|
||||
@include icon-color('delete', 'files', #ff0000);
|
||||
}
|
||||
.nav-icon-deletedsharesOld {
|
||||
@include icon-color('unshare', 'files', variables.$color-black);
|
||||
}
|
||||
.nav-icon-favorites-starred {
|
||||
@include icon-color('starred', 'actions', variables.$color-yellow, 2, true);
|
||||
}
|
||||
|
||||
#app-navigation .nav-files a.nav-icon-files {
|
||||
width: auto;
|
||||
}
|
||||
/* button needs overrides due to navigation styles */
|
||||
#app-navigation .nav-files a.new {
|
||||
width: 40px;
|
||||
|
|
|
|||
1
apps/files/img/app-dark.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" id="mdi-folder" viewBox="0 0 24 24"><path d="M10,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V8C22,6.89 21.1,6 20,6H12L10,4Z" /></svg>
|
||||
|
After Width: | Height: | Size: 188 B |
|
|
@ -1 +1 @@
|
|||
<svg width="32" height="32" version="1.1" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><path d="m3 4c-0.5 0-1 0.5-1 1v22c0 0.52 0.48 1 1 1h26c0.52 0 1-0.482 1-1v-18c0-0.5-0.5-1-1-1h-13l-4-4z" fill="#fff"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="mdi-folder" viewBox="0 0 24 24"><path d="M10,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V8C22,6.89 21.1,6 20,6H12L10,4Z" fill="#fff" /></svg>
|
||||
|
Before Width: | Height: | Size: 222 B After Width: | Height: | Size: 200 B |
|
|
@ -1 +0,0 @@
|
|||
<svg width="24px" height="24px" fill="#000000" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M20 18c1.1 0 1.99-.9 1.99-2L22 6c0-1.1-.9-2-2-2H4c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2H1c-.55 0-1 .45-1 1s.45 1 1 1h22c.55 0 1-.45 1-1s-.45-1-1-1h-3zM5 6h14c.55 0 1 .45 1 1v8c0 .55-.45 1-1 1H5c-.55 0-1-.45-1-1V7c0-.55.45-1 1-1z"/></svg>
|
||||
|
Before Width: | Height: | Size: 344 B |
|
|
@ -1 +0,0 @@
|
|||
<svg width="16" height="16" version="1.1" viewbox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M6.5 1L6 2H3c-.554 0-1 .446-1 1v1h12V3c0-.554-.446-1-1-1h-3l-.5-1zM3 5l.875 9c.06.55.573 1 1.125 1h6c.552 0 1.064-.45 1.125-1L13 5z"/></svg>
|
||||
|
Before Width: | Height: | Size: 247 B |
|
|
@ -1 +0,0 @@
|
|||
<svg width="16" height="16" version="1.1" viewbox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="m8 1 2.4 2.44-3.46 3.5 2.12 2.12 3.5-3.5 2.44 2.44v-7zm-5.25 1.5c-0.68 0-1.25 0.57-1.25 1.25v9.5c0 0.68 0.57 1.25 1.25 1.25h9.5c0.68 0 1.25-0.57 1.25-1.25v-4.25l-1.5-1.5v5.5h-9v-9h5.5l-1.5-1.5z"/></svg>
|
||||
|
Before Width: | Height: | Size: 309 B |
|
|
@ -1 +1 @@
|
|||
<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="m1.5 2c-0.25 0-0.5 0.25-0.5 0.5v11c0 0.26 0.24 0.5 0.5 0.5h13c0.26 0 0.5-0.241 0.5-0.5v-9c0-0.25-0.25-0.5-0.5-0.5h-6.5l-2-2z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="mdi-folder" viewBox="0 0 24 24"><path d="M10,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V8C22,6.89 21.1,6 20,6H12L10,4Z" /></svg>
|
||||
|
Before Width: | Height: | Size: 240 B After Width: | Height: | Size: 188 B |
|
|
@ -1 +0,0 @@
|
|||
<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="m9.2363 2.166-3.1816 3.1836c-0.7071 0.7072-1.0378 1.6182-0.9883 2.457 0.05 0.8389 0.4333 1.5841 0.9883 2.1387l1.4121-1.416c-0.5672-0.5672-0.5444-1.2192 2e-3 -1.7656l3.1812-3.1817c0.52536-0.52536 1.2507-0.52318 1.772-2e-3 0.48245 0.5556 0.52732 1.2382-4e-3 1.7695l-0.82 0.8203c0.555 0.785 0.645 1.3663 0.593 2.2344l1.641-1.6406c1.2374-1.2374 1.2371-3.3645 0-4.6016-1.236-1.2361-3.342-1.2113-4.5957 4e-3zm0.7071 3.8848-1.4141 1.418h4e-3c0.55 0.55 0.50736 1.2582-4e-3 1.7695l-3.1816 3.1817c-0.696 0.59192-1.2985 0.47105-1.7696 0-0.62636-0.62636-0.5-1.2681 0-1.768l0.85-0.8473c-0.556-0.7835-0.6484-1.365-0.5976-2.2324l-1.666 1.666c-1.2393 1.2393-1.2357 3.36 0 4.5957 1.2353 1.2353 3.362 1.2356 4.5976 0l3.1817-3.182c0.7086-0.7083 1.0396-1.6184 0.9906-2.4586-0.048-0.8401-0.432-1.5864-0.9887-2.1407z"/></svg>
|
||||
|
Before Width: | Height: | Size: 910 B |
|
|
@ -1 +0,0 @@
|
|||
<svg width="16" height="16" version="1.1" viewbox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><circle cx="8" cy="8" r="7" fill="none" stroke="#000" stroke-width="2"/><path d="m8 3.5-1 5 3.5 2-2-2z" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/></svg>
|
||||
|
Before Width: | Height: | Size: 289 B |
|
|
@ -1 +0,0 @@
|
|||
<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><circle cx="3.5" cy="8" r="2.5"/><circle cx="12.5" cy="12.5" r="2.5"/><circle cx="12.5" cy="3.5" r="2.5"/><path d="m3.5 8 9 4.5m-9-4.5 9-4.5" fill="none" stroke="#000" stroke-width="2"/></svg>
|
||||
|
Before Width: | Height: | Size: 290 B |
|
|
@ -1 +0,0 @@
|
|||
<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="m8 0.5 2.2 5.3 5.8 0.45-4.5 3.75 1.5 5.5-5-3.1-5 3.1 1.5-5.5-4.5-3.75 5.8-0.45z"/></svg>
|
||||
|
Before Width: | Height: | Size: 195 B |
|
|
@ -1 +0,0 @@
|
|||
<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="m12.5 1a2.5 2.5 0 0 0-2.5 2.5 2.5 2.5 0 0 0 0.003906 0.12891l-4.9023 2.4512a2.5 2.5 0 0 0-1.6016-0.58008 2.5 2.5 0 0 0-2.5 2.5 2.5 2.5 0 0 0 2.5 2.5 2.5 2.5 0 0 0 0.30469-0.021484l3.4395-1.7246-1.25-0.625a2.5 2.5 0 0 0 0.0058594-0.12891 2.5 2.5 0 0 0-0.0039062-0.12891l4.9023-2.4512a2.5 2.5 0 0 0 1.6016 0.58008 2.5 2.5 0 0 0 0.26562-0.013672l1.5625-0.7832a2.5 2.5 0 0 0 0.67188-1.7031 2.5 2.5 0 0 0-2.5-2.5zm0.25391 9.0156-3.7246 1.8672 0.97656 0.48828a2.5 2.5 0 0 0-0.005859 0.12891 2.5 2.5 0 0 0 2.5 2.5 2.5 2.5 0 0 0 2.5-2.5 2.5 2.5 0 0 0-2.2461-2.4844z"/><rect transform="rotate(-26.63)" x="-1.0586" y="11.891" width="11.687" height="2.0029" ry="0" style="paint-order:normal"/></svg>
|
||||
|
Before Width: | Height: | Size: 795 B |
|
|
@ -133,7 +133,6 @@ class Application extends App implements IBootstrap {
|
|||
$context->injectFn([Listener::class, 'register']);
|
||||
$context->injectFn(Closure::fromCallable([$this, 'registerSearchProvider']));
|
||||
$this->registerTemplates();
|
||||
$context->injectFn(Closure::fromCallable([$this, 'registerNavigation']));
|
||||
$this->registerHooks();
|
||||
}
|
||||
|
||||
|
|
@ -152,18 +151,6 @@ class Application extends App implements IBootstrap {
|
|||
$templateManager->registerTemplate('application/vnd.oasis.opendocument.spreadsheet', 'core/templates/filetemplates/template.ods');
|
||||
}
|
||||
|
||||
private function registerNavigation(IL10N $l10n): void {
|
||||
\OCA\Files\App::getNavigationManager()->add(function () use ($l10n) {
|
||||
return [
|
||||
'id' => 'files',
|
||||
'appname' => 'files',
|
||||
'script' => 'list.php',
|
||||
'order' => 0,
|
||||
'name' => $l10n->t('All files')
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
private function registerHooks(): void {
|
||||
Util::connectHook('\OCP\Config', 'js', '\OCA\Files\App', 'extendJsConfig');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -388,6 +388,7 @@ class ApiController extends Controller {
|
|||
/**
|
||||
* @NoAdminRequired
|
||||
* @NoCSRFRequired
|
||||
* @PublicPage
|
||||
*
|
||||
* Get the service-worker Javascript for previews
|
||||
*
|
||||
|
|
|
|||
|
|
@ -153,17 +153,36 @@ class ViewController extends Controller {
|
|||
*
|
||||
* @param string $fileid
|
||||
* @return TemplateResponse|RedirectResponse
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function showFile(string $fileid = null, int $openfile = 1): Response {
|
||||
public function showFile(string $fileid = null): Response {
|
||||
if (!$fileid) {
|
||||
return new RedirectResponse($this->urlGenerator->linkToRoute('files.view.index'));
|
||||
}
|
||||
|
||||
// This is the entry point from the `/f/{fileid}` URL which is hardcoded in the server.
|
||||
try {
|
||||
return $this->redirectToFile($fileid, $openfile !== 0);
|
||||
return $this->redirectToFile((int) $fileid);
|
||||
} catch (NotFoundException $e) {
|
||||
return new RedirectResponse($this->urlGenerator->linkToRoute('files.view.index', ['fileNotFound' => true]));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @NoCSRFRequired
|
||||
* @NoAdminRequired
|
||||
* @UseSession
|
||||
*
|
||||
* @param string $dir
|
||||
* @param string $view
|
||||
* @param string $fileid
|
||||
* @param bool $fileNotFound
|
||||
* @return TemplateResponse|RedirectResponse
|
||||
*/
|
||||
public function indexView($dir = '', $view = '', $fileid = null, $fileNotFound = false) {
|
||||
return $this->index($dir, $view, $fileid, $fileNotFound);
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoCSRFRequired
|
||||
* @NoAdminRequired
|
||||
|
|
@ -173,22 +192,30 @@ class ViewController extends Controller {
|
|||
* @param string $view
|
||||
* @param string $fileid
|
||||
* @param bool $fileNotFound
|
||||
* @param string $openfile - the openfile URL parameter if it was present in the initial request
|
||||
* @return TemplateResponse|RedirectResponse
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function index($dir = '', $view = '', $fileid = null, $fileNotFound = false, $openfile = null) {
|
||||
public function indexViewFileid($dir = '', $view = '', $fileid = null, $fileNotFound = false) {
|
||||
return $this->index($dir, $view, $fileid, $fileNotFound);
|
||||
}
|
||||
|
||||
if ($fileid !== null && $dir === '') {
|
||||
/**
|
||||
* @NoCSRFRequired
|
||||
* @NoAdminRequired
|
||||
* @UseSession
|
||||
*
|
||||
* @param string $dir
|
||||
* @param string $view
|
||||
* @param string $fileid
|
||||
* @param bool $fileNotFound
|
||||
* @return TemplateResponse|RedirectResponse
|
||||
*/
|
||||
public function index($dir = '', $view = '', $fileid = null, $fileNotFound = false) {
|
||||
if ($fileid !== null && $view !== 'trashbin') {
|
||||
try {
|
||||
return $this->redirectToFile($fileid);
|
||||
} catch (NotFoundException $e) {
|
||||
return new RedirectResponse($this->urlGenerator->linkToRoute('files.view.index', ['fileNotFound' => true]));
|
||||
}
|
||||
return $this->redirectToFileIfInTrashbin((int) $fileid);
|
||||
} catch (NotFoundException $e) {}
|
||||
}
|
||||
|
||||
$nav = new \OCP\Template('files', 'appnavigation', '');
|
||||
|
||||
// Load the files we need
|
||||
\OCP\Util::addStyle('files', 'merged');
|
||||
\OCP\Util::addScript('files', 'merged-index', 'files');
|
||||
|
|
@ -203,17 +230,6 @@ class ViewController extends Controller {
|
|||
$favElements['folders'] = [];
|
||||
}
|
||||
|
||||
$navItems = \OCA\Files\App::getNavigationManager()->getAll();
|
||||
|
||||
// parse every menu and add the expanded user value
|
||||
foreach ($navItems as $key => $item) {
|
||||
$navItems[$key]['expanded'] = $this->config->getUserValue($userId, 'files', 'show_' . $item['id'], '0') === '1';
|
||||
}
|
||||
|
||||
$nav->assign('navigationItems', $navItems);
|
||||
|
||||
$contentItems = [];
|
||||
|
||||
try {
|
||||
// If view is files, we use the directory, otherwise we use the root storage
|
||||
$storageInfo = $this->getStorageInfo(($view === 'files' && $dir) ? $dir : '/');
|
||||
|
|
@ -222,7 +238,6 @@ class ViewController extends Controller {
|
|||
}
|
||||
|
||||
$this->initialState->provideInitialState('storageStats', $storageInfo);
|
||||
$this->initialState->provideInitialState('navigation', $navItems);
|
||||
$this->initialState->provideInitialState('config', $this->userConfig->getConfigs());
|
||||
$this->initialState->provideInitialState('viewConfigs', $this->viewConfig->getConfigs());
|
||||
$this->initialState->provideInitialState('favoriteFolders', $favElements['folders'] ?? []);
|
||||
|
|
@ -231,34 +246,9 @@ class ViewController extends Controller {
|
|||
$filesSortingConfig = json_decode($this->config->getUserValue($userId, 'files', 'files_sorting_configs', '{}'), true);
|
||||
$this->initialState->provideInitialState('filesSortingConfig', $filesSortingConfig);
|
||||
|
||||
// render the container content for every navigation item
|
||||
foreach ($navItems as $item) {
|
||||
$content = '';
|
||||
if (isset($item['script'])) {
|
||||
$content = $this->renderScript($item['appname'], $item['script']);
|
||||
}
|
||||
// parse submenus
|
||||
if (isset($item['sublist'])) {
|
||||
foreach ($item['sublist'] as $subitem) {
|
||||
$subcontent = '';
|
||||
if (isset($subitem['script'])) {
|
||||
$subcontent = $this->renderScript($subitem['appname'], $subitem['script']);
|
||||
}
|
||||
$contentItems[$subitem['id']] = [
|
||||
'id' => $subitem['id'],
|
||||
'content' => $subcontent
|
||||
];
|
||||
}
|
||||
}
|
||||
$contentItems[$item['id']] = [
|
||||
'id' => $item['id'],
|
||||
'content' => $content
|
||||
];
|
||||
}
|
||||
|
||||
$this->eventDispatcher->dispatchTyped(new ResourcesLoadAdditionalScriptsEvent());
|
||||
$event = new LoadAdditionalScriptsEvent();
|
||||
$this->eventDispatcher->dispatchTyped($event);
|
||||
$this->eventDispatcher->dispatchTyped(new ResourcesLoadAdditionalScriptsEvent());
|
||||
$this->eventDispatcher->dispatchTyped(new LoadSidebar());
|
||||
// Load Viewer scripts
|
||||
if (class_exists(LoadViewer::class)) {
|
||||
|
|
@ -268,23 +258,9 @@ class ViewController extends Controller {
|
|||
$this->initialState->provideInitialState('templates_path', $this->templateManager->hasTemplateDirectory() ? $this->templateManager->getTemplatePath() : false);
|
||||
$this->initialState->provideInitialState('templates', $this->templateManager->listCreators());
|
||||
|
||||
$params = [];
|
||||
$params['usedSpacePercent'] = (int) $storageInfo['relative'];
|
||||
$params['owner'] = $storageInfo['owner'] ?? '';
|
||||
$params['ownerDisplayName'] = $storageInfo['ownerDisplayName'] ?? '';
|
||||
$params['isPublic'] = false;
|
||||
$params['allowShareWithLink'] = $this->shareManager->shareApiAllowLinks() ? 'yes' : 'no';
|
||||
$params['defaultFileSorting'] = $filesSortingConfig['files']['mode'] ?? 'basename';
|
||||
$params['defaultFileSortingDirection'] = $filesSortingConfig['files']['direction'] ?? 'asc';
|
||||
$params['showgridview'] = $this->config->getUserValue($userId, 'files', 'show_grid', false);
|
||||
$showHidden = (bool) $this->config->getUserValue($userId, 'files', 'show_hidden', false);
|
||||
$params['showHiddenFiles'] = $showHidden ? 1 : 0;
|
||||
$cropImagePreviews = (bool) $this->config->getUserValue($userId, 'files', 'crop_image_previews', true);
|
||||
$params['cropImagePreviews'] = $cropImagePreviews ? 1 : 0;
|
||||
$params['fileNotFound'] = $fileNotFound ? 1 : 0;
|
||||
$params['appNavigation'] = $nav;
|
||||
$params['appContents'] = $contentItems;
|
||||
$params['hiddenFields'] = $event->getHiddenFields();
|
||||
$params = [
|
||||
'fileNotFound' => $fileNotFound ? 1 : 0
|
||||
];
|
||||
|
||||
$response = new TemplateResponse(
|
||||
Application::APP_ID,
|
||||
|
|
@ -293,21 +269,23 @@ class ViewController extends Controller {
|
|||
);
|
||||
$policy = new ContentSecurityPolicy();
|
||||
$policy->addAllowedFrameDomain('\'self\'');
|
||||
// Allow preview service worker
|
||||
$policy->addAllowedWorkerSrcDomain('\'self\'');
|
||||
$response->setContentSecurityPolicy($policy);
|
||||
|
||||
$this->provideInitialState($dir, $openfile);
|
||||
$this->provideInitialState($dir, $fileid);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add openFileInfo in initialState if $openfile is set.
|
||||
* Add openFileInfo in initialState.
|
||||
* @param string $dir - the ?dir= URL param
|
||||
* @param string $openfile - the ?openfile= URL param
|
||||
* @param string $fileid - the fileid URL param
|
||||
* @return void
|
||||
*/
|
||||
private function provideInitialState(string $dir, ?string $openfile): void {
|
||||
if ($openfile === null) {
|
||||
private function provideInitialState(string $dir, ?string $fileid): void {
|
||||
if ($fileid === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -319,7 +297,7 @@ class ViewController extends Controller {
|
|||
|
||||
$uid = $user->getUID();
|
||||
$userFolder = $this->rootFolder->getUserFolder($uid);
|
||||
$nodes = $userFolder->getById((int) $openfile);
|
||||
$nodes = $userFolder->getById((int) $fileid);
|
||||
$node = array_shift($nodes);
|
||||
|
||||
if ($node === null) {
|
||||
|
|
@ -351,44 +329,70 @@ class ViewController extends Controller {
|
|||
}
|
||||
|
||||
/**
|
||||
* Redirects to the file list and highlight the given file id
|
||||
* Redirects to the trashbin file list and highlight the given file id
|
||||
*
|
||||
* @param string $fileId file id to show
|
||||
* @param bool $setOpenfile - whether or not to set the openfile URL parameter
|
||||
* @param int $fileId file id to show
|
||||
* @return RedirectResponse redirect response or not found response
|
||||
* @throws \OCP\Files\NotFoundException
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
private function redirectToFile($fileId, bool $setOpenfile = false) {
|
||||
private function redirectToFileIfInTrashbin($fileId): RedirectResponse {
|
||||
$uid = $this->userSession->getUser()->getUID();
|
||||
$baseFolder = $this->rootFolder->getUserFolder($uid);
|
||||
$files = $baseFolder->getById($fileId);
|
||||
$nodes = $baseFolder->getById($fileId);
|
||||
$params = [];
|
||||
|
||||
if (empty($files) && $this->appManager->isEnabledForUser('files_trashbin')) {
|
||||
if (empty($nodes) && $this->appManager->isEnabledForUser('files_trashbin')) {
|
||||
/** @var Folder */
|
||||
$baseFolder = $this->rootFolder->get($uid . '/files_trashbin/files/');
|
||||
$files = $baseFolder->getById($fileId);
|
||||
$nodes = $baseFolder->getById($fileId);
|
||||
$params['view'] = 'trashbin';
|
||||
}
|
||||
|
||||
if (!empty($files)) {
|
||||
$file = current($files);
|
||||
if ($file instanceof Folder) {
|
||||
if (!empty($nodes)) {
|
||||
$node = current($nodes);
|
||||
$params['fileid'] = $fileId;
|
||||
if ($node instanceof Folder) {
|
||||
// set the full path to enter the folder
|
||||
$params['dir'] = $baseFolder->getRelativePath($node->getPath());
|
||||
} else {
|
||||
// set parent path as dir
|
||||
$params['dir'] = $baseFolder->getRelativePath($node->getParent()->getPath());
|
||||
}
|
||||
return new RedirectResponse($this->urlGenerator->linkToRoute('files.view.indexViewFileid', $params));
|
||||
}
|
||||
}
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirects to the file list and highlight the given file id
|
||||
*
|
||||
* @param int $fileId file id to show
|
||||
* @return RedirectResponse redirect response or not found response
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
private function redirectToFile(int $fileId) {
|
||||
$uid = $this->userSession->getUser()->getUID();
|
||||
$baseFolder = $this->rootFolder->getUserFolder($uid);
|
||||
$nodes = $baseFolder->getById($fileId);
|
||||
$params = [];
|
||||
|
||||
try {
|
||||
$this->redirectToFileIfInTrashbin($fileId);
|
||||
} catch (NotFoundException $e) {}
|
||||
|
||||
if (!empty($nodes)) {
|
||||
$node = current($nodes);
|
||||
$params['fileid'] = $fileId;
|
||||
if ($node instanceof Folder) {
|
||||
// set the full path to enter the folder
|
||||
$params['dir'] = $baseFolder->getRelativePath($file->getPath());
|
||||
$params['dir'] = $baseFolder->getRelativePath($node->getPath());
|
||||
} else {
|
||||
// set parent path as dir
|
||||
$params['dir'] = $baseFolder->getRelativePath($file->getParent()->getPath());
|
||||
// and scroll to the entry
|
||||
$params['scrollto'] = $file->getName();
|
||||
|
||||
if ($setOpenfile) {
|
||||
// forward the openfile URL parameter.
|
||||
$params['openfile'] = $fileId;
|
||||
}
|
||||
$params['dir'] = $baseFolder->getRelativePath($node->getParent()->getPath());
|
||||
}
|
||||
|
||||
return new RedirectResponse($this->urlGenerator->linkToRoute('files.view.index', $params));
|
||||
return new RedirectResponse($this->urlGenerator->linkToRoute('files.view.indexViewFileid', $params));
|
||||
}
|
||||
throw new \OCP\Files\NotFoundException();
|
||||
|
||||
throw new NotFoundException();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,18 +31,7 @@ use OCP\EventDispatcher\Event;
|
|||
|
||||
/**
|
||||
* This event is triggered when the files app is rendered.
|
||||
* It can be used to add additional scripts to the files app.
|
||||
*
|
||||
* @since 17.0.0
|
||||
*/
|
||||
class LoadAdditionalScriptsEvent extends Event {
|
||||
private $hiddenFields = [];
|
||||
|
||||
public function addHiddenField(string $name, string $value): void {
|
||||
$this->hiddenFields[$name] = $value;
|
||||
}
|
||||
|
||||
public function getHiddenFields(): array {
|
||||
return $this->hiddenFields;
|
||||
}
|
||||
}
|
||||
class LoadAdditionalScriptsEvent extends Event {}
|
||||
|
|
@ -340,6 +340,7 @@
|
|||
"api"
|
||||
],
|
||||
"security": [
|
||||
{},
|
||||
{
|
||||
"bearer_auth": []
|
||||
},
|
||||
|
|
|
|||
|
|
@ -19,11 +19,13 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
import { action } from './openFolderAction'
|
||||
import type { Navigation } from '../services/Navigation'
|
||||
|
||||
import { expect } from '@jest/globals'
|
||||
import { File, Folder, Node, Permission } from '@nextcloud/files'
|
||||
|
||||
import { action } from './openFolderAction'
|
||||
import { DefaultType, FileAction } from '../services/FileAction'
|
||||
import type { Navigation } from '../services/Navigation'
|
||||
|
||||
const view = {
|
||||
id: 'files',
|
||||
|
|
@ -132,7 +134,7 @@ describe('Open folder action execute tests', () => {
|
|||
// Silent action
|
||||
expect(exec).toBe(null)
|
||||
expect(goToRouteMock).toBeCalledTimes(1)
|
||||
expect(goToRouteMock).toBeCalledWith(null, null, { dir: '/FooBar' })
|
||||
expect(goToRouteMock).toBeCalledWith(null, { fileid: undefined, view: 'files' }, { dir: '/FooBar' })
|
||||
})
|
||||
|
||||
test('Open folder fails without node', async () => {
|
||||
|
|
|
|||
|
|
@ -59,8 +59,8 @@ export const action = new FileAction({
|
|||
|
||||
window.OCP.Files.Router.goToRoute(
|
||||
null,
|
||||
null,
|
||||
{ dir: join(dir, node.basename) },
|
||||
{ view: view.id, fileid: undefined },
|
||||
{ dir: join(dir, node.basename), fileid: undefined },
|
||||
)
|
||||
return null
|
||||
},
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ describe('Open in files action execute tests', () => {
|
|||
// Silent action
|
||||
expect(exec).toBe(null)
|
||||
expect(goToRouteMock).toBeCalledTimes(1)
|
||||
expect(goToRouteMock).toBeCalledWith(null, { fileid: 1, view: 'files' }, { fileid: 1, dir: '/Foo', openfile: true })
|
||||
expect(goToRouteMock).toBeCalledWith(null, { fileid: 1, view: 'files' }, { fileid: 1, dir: '/Foo' })
|
||||
})
|
||||
|
||||
test('Open in files with folder', async () => {
|
||||
|
|
@ -98,6 +98,6 @@ describe('Open in files action execute tests', () => {
|
|||
// Silent action
|
||||
expect(exec).toBe(null)
|
||||
expect(goToRouteMock).toBeCalledTimes(1)
|
||||
expect(goToRouteMock).toBeCalledWith(null, { fileid: 1, view: 'files' }, { fileid: 1, dir: '/Foo/Bar', openfile: true })
|
||||
expect(goToRouteMock).toBeCalledWith(null, { fileid: 1, view: 'files' }, { fileid: 1, dir: '/Foo/Bar' })
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ export const action = new FileAction({
|
|||
window.OCP.Files.Router.goToRoute(
|
||||
null, // use default route
|
||||
{ view: 'files', fileid: node.fileid },
|
||||
{ dir, fileid: node.fileid, openfile: true },
|
||||
{ dir, fileid: node.fileid },
|
||||
)
|
||||
return null
|
||||
},
|
||||
|
|
|
|||
|
|
@ -19,11 +19,13 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
import { action } from './sidebarAction'
|
||||
import type { Navigation } from '../services/Navigation'
|
||||
|
||||
import { expect } from '@jest/globals'
|
||||
import { File, Permission } from '@nextcloud/files'
|
||||
|
||||
import { action } from './sidebarAction'
|
||||
import { FileAction } from '../services/FileAction'
|
||||
import type { Navigation } from '../services/Navigation'
|
||||
import logger from '../logger'
|
||||
|
||||
const view = {
|
||||
|
|
@ -127,6 +129,8 @@ describe('Open sidebar action exec tests', () => {
|
|||
test('Open sidebar', async () => {
|
||||
const openMock = jest.fn()
|
||||
window.OCA = { Files: { Sidebar: { open: openMock } } }
|
||||
const goToRouteMock = jest.fn()
|
||||
window.OCP = { Files: { Router: { goToRoute: goToRouteMock } } }
|
||||
|
||||
const file = new File({
|
||||
id: 1,
|
||||
|
|
@ -139,6 +143,12 @@ describe('Open sidebar action exec tests', () => {
|
|||
// Silent action
|
||||
expect(exec).toBe(null)
|
||||
expect(openMock).toBeCalledWith('/foobar.txt')
|
||||
expect(goToRouteMock).toBeCalledWith(
|
||||
null,
|
||||
{ view: view.id, fileid: 1 },
|
||||
{ dir: '/' },
|
||||
true,
|
||||
)
|
||||
})
|
||||
|
||||
test('Open sidebar fails', async () => {
|
||||
|
|
|
|||
|
|
@ -19,9 +19,11 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
import type { Navigation } from '../services/Navigation'
|
||||
|
||||
import { Permission, type Node } from '@nextcloud/files'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import InformationSvg from '@mdi/svg/svg/information-variant.svg?raw'
|
||||
import { Permission, type Node } from '@nextcloud/files'
|
||||
|
||||
import { registerFileAction, FileAction } from '../services/FileAction'
|
||||
import logger from '../logger.js'
|
||||
|
|
@ -40,6 +42,10 @@ export const action = new FileAction({
|
|||
return false
|
||||
}
|
||||
|
||||
if (!nodes[0]) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Only work if the sidebar is available
|
||||
if (!window?.OCA?.Files?.Sidebar) {
|
||||
return false
|
||||
|
|
@ -48,10 +54,18 @@ export const action = new FileAction({
|
|||
return (nodes[0].root?.startsWith('/files/') && nodes[0].permissions !== Permission.NONE) ?? false
|
||||
},
|
||||
|
||||
async exec(node: Node) {
|
||||
async exec(node: Node, view: Navigation) {
|
||||
try {
|
||||
// TODO: migrate Sidebar to use a Node instead
|
||||
window?.OCA?.Files?.Sidebar?.open?.(node.path)
|
||||
await window.OCA.Files.Sidebar.open(node.path)
|
||||
|
||||
// Silently update current fileid
|
||||
window.OCP.Files.Router.goToRoute(
|
||||
null,
|
||||
{ view: view.id, fileid: node.fileid },
|
||||
{ dir: node.dirname },
|
||||
true,
|
||||
)
|
||||
|
||||
return null
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ describe('View in folder action execute tests', () => {
|
|||
// Silent action
|
||||
expect(exec).toBe(null)
|
||||
expect(goToRouteMock).toBeCalledTimes(1)
|
||||
expect(goToRouteMock).toBeCalledWith(null, { fileid: 1, view: 'files' }, { fileid: 1, dir: '/' })
|
||||
expect(goToRouteMock).toBeCalledWith(null, { fileid: 1, view: 'files' }, { dir: '/' })
|
||||
})
|
||||
|
||||
test('View in (sub) folder', async () => {
|
||||
|
|
@ -148,7 +148,7 @@ describe('View in folder action execute tests', () => {
|
|||
// Silent action
|
||||
expect(exec).toBe(null)
|
||||
expect(goToRouteMock).toBeCalledTimes(1)
|
||||
expect(goToRouteMock).toBeCalledWith(null, { fileid: 1, view: 'files' }, { fileid: 1, dir: '/Foo/Bar' })
|
||||
expect(goToRouteMock).toBeCalledWith(null, { fileid: 1, view: 'files' }, { dir: '/Foo/Bar' })
|
||||
})
|
||||
|
||||
test('View in folder fails without node', async () => {
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ export const action = new FileAction({
|
|||
window.OCP.Files.Router.goToRoute(
|
||||
null,
|
||||
{ view: 'files', fileid: node.fileid },
|
||||
{ dir: node.dirname, fileid: node.fileid },
|
||||
{ dir: node.dirname },
|
||||
)
|
||||
return null
|
||||
},
|
||||
|
|
|
|||
|
|
@ -21,9 +21,18 @@
|
|||
-->
|
||||
|
||||
<template>
|
||||
<Fragment>
|
||||
<tr :class="{'files-list__row--visible': visible, 'files-list__row--active': isActive}"
|
||||
data-cy-files-list-row
|
||||
:data-cy-files-list-row-fileid="fileid"
|
||||
:data-cy-files-list-row-name="source.basename"
|
||||
class="list__row"
|
||||
@contextmenu="onRightClick">
|
||||
<!-- Failed indicator -->
|
||||
<span v-if="source.attributes.failed" class="files-list__row--failed" />
|
||||
|
||||
<!-- Checkbox -->
|
||||
<td class="files-list__row-checkbox">
|
||||
<NcCheckboxRadioSwitch v-if="active"
|
||||
<NcCheckboxRadioSwitch v-if="visible"
|
||||
:aria-label="t('files', 'Select the row for {displayName}', { displayName })"
|
||||
:checked="selectedFiles"
|
||||
:value="fileid"
|
||||
|
|
@ -32,7 +41,7 @@
|
|||
</td>
|
||||
|
||||
<!-- Link to file -->
|
||||
<td class="files-list__row-name">
|
||||
<td class="files-list__row-name" data-cy-files-list-row-name>
|
||||
<!-- Icon or preview -->
|
||||
<span class="files-list__row-icon" @click="execDefaultAction">
|
||||
<FolderIcon v-if="source.type === 'folder'" />
|
||||
|
|
@ -43,10 +52,6 @@
|
|||
class="files-list__row-icon-preview"
|
||||
:style="{ backgroundImage }" />
|
||||
|
||||
<span v-else-if="mimeIconUrl"
|
||||
class="files-list__row-icon-preview files-list__row-icon-preview--mime"
|
||||
:style="{ backgroundImage: mimeIconUrl }" />
|
||||
|
||||
<FileIcon v-else />
|
||||
|
||||
<!-- Favorite icon -->
|
||||
|
|
@ -79,6 +84,7 @@
|
|||
ref="basename"
|
||||
:aria-hidden="isRenaming"
|
||||
class="files-list__row-name-link"
|
||||
data-cy-files-list-row-name-link
|
||||
v-bind="linkTo"
|
||||
@click="execDefaultAction">
|
||||
<!-- File name -->
|
||||
|
|
@ -91,7 +97,10 @@
|
|||
</td>
|
||||
|
||||
<!-- Actions -->
|
||||
<td v-show="!isRenamingSmallScreen" :class="`files-list__row-actions-${uniqueId}`" class="files-list__row-actions">
|
||||
<td v-show="!isRenamingSmallScreen"
|
||||
:class="`files-list__row-actions-${uniqueId}`"
|
||||
class="files-list__row-actions"
|
||||
data-cy-files-list-row-actions>
|
||||
<!-- Render actions -->
|
||||
<CustomElementRender v-for="action in enabledRenderActions"
|
||||
:key="action.id"
|
||||
|
|
@ -100,10 +109,10 @@
|
|||
:source="source" />
|
||||
|
||||
<!-- Menu actions -->
|
||||
<NcActions v-if="active"
|
||||
<NcActions v-if="visible"
|
||||
ref="actionsMenu"
|
||||
:boundaries-element="boundariesElement"
|
||||
:container="boundariesElement"
|
||||
:boundaries-element="getBoundariesElement()"
|
||||
:container="getBoundariesElement()"
|
||||
:disabled="source._loading"
|
||||
:force-name="true"
|
||||
:force-menu="enabledInlineActions.length === 0 /* forceMenu only if no inline actions */"
|
||||
|
|
@ -113,6 +122,7 @@
|
|||
:key="action.id"
|
||||
:class="'files-list__row-action-' + action.id"
|
||||
:close-after-click="true"
|
||||
:data-cy-files-list-row-action="action.id"
|
||||
@click="onActionClick(action)">
|
||||
<template #icon>
|
||||
<NcLoadingIcon v-if="loading === action.id" :size="18" />
|
||||
|
|
@ -127,6 +137,7 @@
|
|||
<td v-if="isSizeAvailable"
|
||||
:style="{ opacity: sizeOpacity }"
|
||||
class="files-list__row-size"
|
||||
data-cy-files-list-row-size
|
||||
@click="openDetailsIfAvailable">
|
||||
<span>{{ size }}</span>
|
||||
</td>
|
||||
|
|
@ -134,6 +145,7 @@
|
|||
<!-- Mtime -->
|
||||
<td v-if="isMtimeAvailable"
|
||||
class="files-list__row-mtime"
|
||||
data-cy-files-list-row-mtime
|
||||
@click="openDetailsIfAvailable">
|
||||
<span>{{ mtime }}</span>
|
||||
</td>
|
||||
|
|
@ -143,36 +155,36 @@
|
|||
:key="column.id"
|
||||
:class="`files-list__row-${currentView?.id}-${column.id}`"
|
||||
class="files-list__row-column-custom"
|
||||
:data-cy-files-list-row-column-custom="column.id"
|
||||
@click="openDetailsIfAvailable">
|
||||
<CustomElementRender v-if="active"
|
||||
<CustomElementRender v-if="visible"
|
||||
:current-view="currentView"
|
||||
:render="column.render"
|
||||
:source="source" />
|
||||
</td>
|
||||
</Fragment>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<script lang='ts'>
|
||||
import { CancelablePromise } from 'cancelable-promise'
|
||||
import { debounce } from 'debounce'
|
||||
import { emit } from '@nextcloud/event-bus'
|
||||
import { formatFileSize, Permission } from '@nextcloud/files'
|
||||
import { Fragment } from 'vue-frag'
|
||||
import { extname } from 'path'
|
||||
import { formatFileSize, Permission } from '@nextcloud/files'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { showError, showSuccess } from '@nextcloud/dialogs'
|
||||
import { translate } from '@nextcloud/l10n'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { vOnClickOutside } from '@vueuse/components'
|
||||
import axios from '@nextcloud/axios'
|
||||
import CancelablePromise from 'cancelable-promise'
|
||||
import FileIcon from 'vue-material-design-icons/File.vue'
|
||||
import FolderIcon from 'vue-material-design-icons/Folder.vue'
|
||||
import moment from '@nextcloud/moment'
|
||||
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
|
||||
import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
|
||||
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
|
||||
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
|
||||
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
|
||||
import Vue from 'vue'
|
||||
import type moment from 'moment'
|
||||
|
||||
import { ACTION_DETAILS } from '../actions/sidebarAction.ts'
|
||||
import { getFileActions, DefaultType } from '../services/FileAction.ts'
|
||||
|
|
@ -181,9 +193,9 @@ import { isCachedPreview } from '../services/PreviewService.ts'
|
|||
import { useActionsMenuStore } from '../store/actionsmenu.ts'
|
||||
import { useFilesStore } from '../store/files.ts'
|
||||
import { useKeyboardStore } from '../store/keyboard.ts'
|
||||
import { useRenamingStore } from '../store/renaming.ts'
|
||||
import { useSelectionStore } from '../store/selection.ts'
|
||||
import { useUserConfigStore } from '../store/userconfig.ts'
|
||||
import { useRenamingStore } from '../store/renaming.ts'
|
||||
import CustomElementRender from './CustomElementRender.vue'
|
||||
import CustomSvgIconRender from './CustomSvgIconRender.vue'
|
||||
import FavoriteIcon from './FavoriteIcon.vue'
|
||||
|
|
@ -203,7 +215,6 @@ export default Vue.extend({
|
|||
FavoriteIcon,
|
||||
FileIcon,
|
||||
FolderIcon,
|
||||
Fragment,
|
||||
NcActionButton,
|
||||
NcActions,
|
||||
NcCheckboxRadioSwitch,
|
||||
|
|
@ -212,7 +223,7 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
props: {
|
||||
active: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
|
@ -263,7 +274,6 @@ export default Vue.extend({
|
|||
return {
|
||||
backgroundFailed: false,
|
||||
backgroundImage: '',
|
||||
boundariesElement: document.querySelector('.app-content > .files-list'),
|
||||
loading: '',
|
||||
}
|
||||
},
|
||||
|
|
@ -284,10 +294,13 @@ export default Vue.extend({
|
|||
return this.currentView?.columns || []
|
||||
},
|
||||
|
||||
dir() {
|
||||
currentDir() {
|
||||
// Remove any trailing slash but leave root slash
|
||||
return (this.$route?.query?.dir || '/').replace(/^(.+)\/$/, '$1')
|
||||
},
|
||||
currentFileId() {
|
||||
return this.$route.params.fileid || this.$route.query.fileid || null
|
||||
},
|
||||
fileid() {
|
||||
return this.source?.fileid?.toString?.()
|
||||
},
|
||||
|
|
@ -342,6 +355,13 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
linkTo() {
|
||||
if (this.source.attributes.failed) {
|
||||
return {
|
||||
title: this.t('files', 'This node is unavailable'),
|
||||
is: 'span',
|
||||
}
|
||||
}
|
||||
|
||||
if (this.enabledDefaultActions.length > 0) {
|
||||
const action = this.enabledDefaultActions[0]
|
||||
const displayName = action.displayName([this.source], this.currentView)
|
||||
|
|
@ -368,7 +388,7 @@ export default Vue.extend({
|
|||
return this.selectionStore.selected
|
||||
},
|
||||
isSelected() {
|
||||
return this.selectedFiles.includes(this.source?.fileid?.toString?.())
|
||||
return this.selectedFiles.includes(this.fileid)
|
||||
},
|
||||
|
||||
cropPreviews() {
|
||||
|
|
@ -385,6 +405,7 @@ export default Vue.extend({
|
|||
// Request tiny previews
|
||||
url.searchParams.set('x', '32')
|
||||
url.searchParams.set('y', '32')
|
||||
url.searchParams.set('mimeFallback', 'true')
|
||||
|
||||
// Handle cropping
|
||||
url.searchParams.set('a', this.cropPreviews === true ? '0' : '1')
|
||||
|
|
@ -393,17 +414,13 @@ export default Vue.extend({
|
|||
return null
|
||||
}
|
||||
},
|
||||
mimeIconUrl() {
|
||||
const mimeType = this.source.mime || 'application/octet-stream'
|
||||
const mimeIconUrl = window.OC?.MimeType?.getIconUrl?.(mimeType)
|
||||
if (mimeIconUrl) {
|
||||
return `url(${mimeIconUrl})`
|
||||
}
|
||||
return ''
|
||||
},
|
||||
|
||||
// Sorted actions that are enabled for this node
|
||||
enabledActions() {
|
||||
if (this.source.attributes.failed) {
|
||||
return []
|
||||
}
|
||||
|
||||
return actions
|
||||
.filter(action => !action.enabled || action.enabled([this.source], this.currentView))
|
||||
.sort((a, b) => (a.order || 0) - (b.order || 0))
|
||||
|
|
@ -419,7 +436,7 @@ export default Vue.extend({
|
|||
|
||||
// Enabled action that are displayed inline with a custom render function
|
||||
enabledRenderActions() {
|
||||
if (!this.active) {
|
||||
if (!this.visible) {
|
||||
return []
|
||||
}
|
||||
return this.enabledActions.filter(action => typeof action.renderInline === 'function')
|
||||
|
|
@ -473,24 +490,13 @@ export default Vue.extend({
|
|||
this.renamingStore.newName = newName
|
||||
},
|
||||
},
|
||||
|
||||
isActive() {
|
||||
return this.fileid === this.currentFileId?.toString?.()
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
active(active, before) {
|
||||
if (active === false && before === true) {
|
||||
this.resetState()
|
||||
|
||||
// When the row is not active anymore
|
||||
// remove the display from the row to prevent
|
||||
// keyboard interaction with it.
|
||||
this.$el.parentNode.style.display = 'none'
|
||||
return
|
||||
}
|
||||
|
||||
// Restore default tabindex
|
||||
this.$el.parentNode.style.display = ''
|
||||
},
|
||||
|
||||
/**
|
||||
* When the source changes, reset the preview
|
||||
* and fetch the new one.
|
||||
|
|
@ -522,9 +528,6 @@ export default Vue.extend({
|
|||
|
||||
// Fetch the preview on init
|
||||
this.debounceIfNotCached()
|
||||
|
||||
// Right click watcher on tr
|
||||
this.$el.parentNode?.addEventListener?.('contextmenu', this.onRightClick)
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
|
|
@ -563,8 +566,8 @@ export default Vue.extend({
|
|||
// Store the promise to be able to cancel it
|
||||
this.previewPromise = new CancelablePromise((resolve, reject, onCancel) => {
|
||||
const img = new Image()
|
||||
// If active, load the preview with higher priority
|
||||
img.fetchpriority = this.active ? 'high' : 'auto'
|
||||
// If visible, load the preview with higher priority
|
||||
img.fetchpriority = this.visible ? 'high' : 'auto'
|
||||
img.onload = () => {
|
||||
this.backgroundImage = `url(${this.previewUrl})`
|
||||
this.backgroundFailed = false
|
||||
|
|
@ -613,7 +616,7 @@ export default Vue.extend({
|
|||
this.loading = action.id
|
||||
Vue.set(this.source, '_loading', true)
|
||||
|
||||
const success = await action.exec(this.source, this.currentView, this.dir)
|
||||
const success = await action.exec(this.source, this.currentView, this.currentDir)
|
||||
|
||||
// If the action returns null, we stay silent
|
||||
if (success === null) {
|
||||
|
|
@ -639,7 +642,7 @@ export default Vue.extend({
|
|||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
// Execute the first default action if any
|
||||
this.enabledDefaultActions[0].exec(this.source, this.currentView, this.dir)
|
||||
this.enabledDefaultActions[0].exec(this.source, this.currentView, this.currentDir)
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -816,7 +819,7 @@ export default Vue.extend({
|
|||
showError(this.t('files', 'Could not rename "{oldName}", it does not exist any more', { oldName }))
|
||||
return
|
||||
} else if (error?.response?.status === 412) {
|
||||
showError(this.t('files', 'The name "{newName}"" is already used in the folder "{dir}". Please choose a different name.', { newName, dir: this.dir }))
|
||||
showError(this.t('files', 'The name "{newName}"" is already used in the folder "{dir}". Please choose a different name.', { newName, dir: this.currentDir }))
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -828,6 +831,15 @@ export default Vue.extend({
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Making this a function in case the files-list
|
||||
* reference changes in the future. That way we're
|
||||
* sure there is one at the time we call it.
|
||||
*/
|
||||
getBoundariesElement() {
|
||||
return document.querySelector('.app-content > .files-list')
|
||||
},
|
||||
|
||||
t: translate,
|
||||
formatFileSize,
|
||||
},
|
||||
|
|
@ -839,7 +851,7 @@ export default Vue.extend({
|
|||
tr {
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
&:visible {
|
||||
background-color: var(--color-background-dark);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,194 +20,51 @@
|
|||
-
|
||||
-->
|
||||
<template>
|
||||
<tr>
|
||||
<th class="files-list__column files-list__row-checkbox">
|
||||
<NcCheckboxRadioSwitch v-bind="selectAllBind" @update:checked="onToggleAll" />
|
||||
</th>
|
||||
|
||||
<!-- Actions multiple if some are selected -->
|
||||
<FilesListHeaderActions v-if="!isNoneSelected"
|
||||
:current-view="currentView"
|
||||
:selected-nodes="selectedNodes" />
|
||||
|
||||
<!-- Columns display -->
|
||||
<template v-else>
|
||||
<!-- Link to file -->
|
||||
<th class="files-list__column files-list__row-name files-list__column--sortable"
|
||||
@click.stop.prevent="toggleSortBy('basename')">
|
||||
<!-- Icon or preview -->
|
||||
<span class="files-list__row-icon" />
|
||||
|
||||
<!-- Name -->
|
||||
<FilesListHeaderButton :name="t('files', 'Name')" mode="basename" />
|
||||
</th>
|
||||
|
||||
<!-- Actions -->
|
||||
<th class="files-list__row-actions" />
|
||||
|
||||
<!-- Size -->
|
||||
<th v-if="isSizeAvailable"
|
||||
:class="{'files-list__column--sortable': isSizeAvailable}"
|
||||
class="files-list__column files-list__row-size">
|
||||
<FilesListHeaderButton :name="t('files', 'Size')" mode="size" />
|
||||
</th>
|
||||
|
||||
<!-- Mtime -->
|
||||
<th v-if="isMtimeAvailable"
|
||||
:class="{'files-list__column--sortable': isMtimeAvailable}"
|
||||
class="files-list__column files-list__row-mtime">
|
||||
<FilesListHeaderButton :name="t('files', 'Modified')" mode="mtime" />
|
||||
</th>
|
||||
|
||||
<!-- Custom views columns -->
|
||||
<th v-for="column in columns"
|
||||
:key="column.id"
|
||||
:class="classForColumn(column)">
|
||||
<FilesListHeaderButton v-if="!!column.sort" :name="column.title" :mode="column.id" />
|
||||
<span v-else>
|
||||
{{ column.title }}
|
||||
</span>
|
||||
</th>
|
||||
</template>
|
||||
</tr>
|
||||
<div v-show="enabled" :class="`files-list__header-${header.id}`">
|
||||
<span ref="mount" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { translate } from '@nextcloud/l10n'
|
||||
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
|
||||
import Vue from 'vue'
|
||||
|
||||
import { useFilesStore } from '../store/files.ts'
|
||||
import { useSelectionStore } from '../store/selection.ts'
|
||||
import FilesListHeaderActions from './FilesListHeaderActions.vue'
|
||||
import FilesListHeaderButton from './FilesListHeaderButton.vue'
|
||||
import filesSortingMixin from '../mixins/filesSorting.ts'
|
||||
import logger from '../logger.js'
|
||||
|
||||
export default Vue.extend({
|
||||
/**
|
||||
* This component is used to render custom
|
||||
* elements provided by an API. Vue doesn't allow
|
||||
* to directly render an HTMLElement, so we can do
|
||||
* this magic here.
|
||||
*/
|
||||
export default {
|
||||
name: 'FilesListHeader',
|
||||
|
||||
components: {
|
||||
FilesListHeaderButton,
|
||||
NcCheckboxRadioSwitch,
|
||||
FilesListHeaderActions,
|
||||
},
|
||||
|
||||
mixins: [
|
||||
filesSortingMixin,
|
||||
],
|
||||
|
||||
props: {
|
||||
isMtimeAvailable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isSizeAvailable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
nodes: {
|
||||
type: Array,
|
||||
header: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
filesListWidth: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
currentFolder: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
currentView: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
setup() {
|
||||
const filesStore = useFilesStore()
|
||||
const selectionStore = useSelectionStore()
|
||||
return {
|
||||
filesStore,
|
||||
selectionStore,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
currentView() {
|
||||
return this.$navigation.active
|
||||
},
|
||||
|
||||
columns() {
|
||||
// Hide columns if the list is too small
|
||||
if (this.filesListWidth < 512) {
|
||||
return []
|
||||
}
|
||||
return this.currentView?.columns || []
|
||||
},
|
||||
|
||||
dir() {
|
||||
// Remove any trailing slash but leave root slash
|
||||
return (this.$route?.query?.dir || '/').replace(/^(.+)\/$/, '$1')
|
||||
},
|
||||
|
||||
selectAllBind() {
|
||||
const label = this.isNoneSelected || this.isSomeSelected
|
||||
? this.t('files', 'Select all')
|
||||
: this.t('files', 'Unselect all')
|
||||
return {
|
||||
'aria-label': label,
|
||||
checked: this.isAllSelected,
|
||||
indeterminate: this.isSomeSelected,
|
||||
title: label,
|
||||
}
|
||||
},
|
||||
|
||||
selectedNodes() {
|
||||
return this.selectionStore.selected
|
||||
},
|
||||
|
||||
isAllSelected() {
|
||||
return this.selectedNodes.length === this.nodes.length
|
||||
},
|
||||
|
||||
isNoneSelected() {
|
||||
return this.selectedNodes.length === 0
|
||||
},
|
||||
|
||||
isSomeSelected() {
|
||||
return !this.isAllSelected && !this.isNoneSelected
|
||||
enabled() {
|
||||
console.debug('Enabled', this.header.id)
|
||||
return this.header.enabled(this.currentFolder, this.currentView)
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
classForColumn(column) {
|
||||
return {
|
||||
'files-list__column': true,
|
||||
'files-list__column--sortable': !!column.sort,
|
||||
'files-list__row-column-custom': true,
|
||||
[`files-list__row-${this.currentView.id}-${column.id}`]: true,
|
||||
watch: {
|
||||
enabled(enabled) {
|
||||
if (!enabled) {
|
||||
return
|
||||
}
|
||||
this.header.updated(this.currentFolder, this.currentView)
|
||||
},
|
||||
|
||||
onToggleAll(selected) {
|
||||
if (selected) {
|
||||
const selection = this.nodes.map(node => node.fileid.toString())
|
||||
logger.debug('Added all nodes to selection', { selection })
|
||||
this.selectionStore.setLastIndex(null)
|
||||
this.selectionStore.set(selection)
|
||||
} else {
|
||||
logger.debug('Cleared selection')
|
||||
this.selectionStore.reset()
|
||||
}
|
||||
},
|
||||
|
||||
t: translate,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.files-list__column {
|
||||
user-select: none;
|
||||
// Make sure the cell colors don't apply to column headers
|
||||
color: var(--color-text-maxcontrast) !important;
|
||||
|
||||
&--sortable {
|
||||
cursor: pointer;
|
||||
}
|
||||
mounted() {
|
||||
console.debug('Mounted', this.header.id)
|
||||
this.header.render(this.$refs.mount, this.currentFolder, this.currentView)
|
||||
},
|
||||
}
|
||||
|
||||
</style>
|
||||
</script>
|
||||
|
|
|
|||
175
apps/files/src/components/FilesListTableFooter.vue
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
<!--
|
||||
- @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
-
|
||||
- @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
-
|
||||
- @license GNU AGPL version 3 or any later version
|
||||
-
|
||||
- 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/>.
|
||||
-
|
||||
-->
|
||||
<template>
|
||||
<tr>
|
||||
<th class="files-list__row-checkbox">
|
||||
<span class="hidden-visually">{{ t('files', 'Total rows summary') }}</span>
|
||||
</th>
|
||||
|
||||
<!-- Link to file -->
|
||||
<td class="files-list__row-name">
|
||||
<!-- Icon or preview -->
|
||||
<span class="files-list__row-icon" />
|
||||
|
||||
<!-- Summary -->
|
||||
<span>{{ summary }}</span>
|
||||
</td>
|
||||
|
||||
<!-- Actions -->
|
||||
<td class="files-list__row-actions" />
|
||||
|
||||
<!-- Size -->
|
||||
<td v-if="isSizeAvailable"
|
||||
class="files-list__column files-list__row-size">
|
||||
<span>{{ totalSize }}</span>
|
||||
</td>
|
||||
|
||||
<!-- Mtime -->
|
||||
<td v-if="isMtimeAvailable"
|
||||
class="files-list__column files-list__row-mtime" />
|
||||
|
||||
<!-- Custom views columns -->
|
||||
<th v-for="column in columns"
|
||||
:key="column.id"
|
||||
:class="classForColumn(column)">
|
||||
<span>{{ column.summary?.(nodes, currentView) }}</span>
|
||||
</th>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { formatFileSize } from '@nextcloud/files'
|
||||
import { translate } from '@nextcloud/l10n'
|
||||
import Vue from 'vue'
|
||||
|
||||
import { useFilesStore } from '../store/files.ts'
|
||||
import { usePathsStore } from '../store/paths.ts'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'FilesListTableFooter',
|
||||
|
||||
components: {
|
||||
},
|
||||
|
||||
props: {
|
||||
isMtimeAvailable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isSizeAvailable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
nodes: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
summary: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
filesListWidth: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
|
||||
setup() {
|
||||
const pathsStore = usePathsStore()
|
||||
const filesStore = useFilesStore()
|
||||
return {
|
||||
filesStore,
|
||||
pathsStore,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
currentView() {
|
||||
return this.$navigation.active
|
||||
},
|
||||
|
||||
dir() {
|
||||
// Remove any trailing slash but leave root slash
|
||||
return (this.$route?.query?.dir || '/').replace(/^(.+)\/$/, '$1')
|
||||
},
|
||||
|
||||
currentFolder() {
|
||||
if (!this.currentView?.id) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.dir === '/') {
|
||||
return this.filesStore.getRoot(this.currentView.id)
|
||||
}
|
||||
const fileId = this.pathsStore.getPath(this.currentView.id, this.dir)
|
||||
return this.filesStore.getNode(fileId)
|
||||
},
|
||||
|
||||
columns() {
|
||||
// Hide columns if the list is too small
|
||||
if (this.filesListWidth < 512) {
|
||||
return []
|
||||
}
|
||||
return this.currentView?.columns || []
|
||||
},
|
||||
|
||||
totalSize() {
|
||||
// If we have the size already, let's use it
|
||||
if (this.currentFolder?.size) {
|
||||
return formatFileSize(this.currentFolder.size, true)
|
||||
}
|
||||
|
||||
// Otherwise let's compute it
|
||||
return formatFileSize(this.nodes.reduce((total, node) => total + node.size || 0, 0), true)
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
classForColumn(column) {
|
||||
return {
|
||||
'files-list__row-column-custom': true,
|
||||
[`files-list__row-${this.currentView.id}-${column.id}`]: true,
|
||||
}
|
||||
},
|
||||
|
||||
t: translate,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
// Scoped row
|
||||
tr {
|
||||
padding-bottom: 300px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
// Prevent hover effect on the whole row
|
||||
background-color: transparent !important;
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
td {
|
||||
user-select: none;
|
||||
// Make sure the cell colors don't apply to column headers
|
||||
color: var(--color-text-maxcontrast) !important;
|
||||
}
|
||||
|
||||
</style>
|
||||
213
apps/files/src/components/FilesListTableHeader.vue
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
<!--
|
||||
- @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
-
|
||||
- @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
-
|
||||
- @license GNU AGPL version 3 or any later version
|
||||
-
|
||||
- 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/>.
|
||||
-
|
||||
-->
|
||||
<template>
|
||||
<tr class="files-list__row-head">
|
||||
<th class="files-list__column files-list__row-checkbox">
|
||||
<NcCheckboxRadioSwitch v-bind="selectAllBind" @update:checked="onToggleAll" />
|
||||
</th>
|
||||
|
||||
<!-- Actions multiple if some are selected -->
|
||||
<FilesListTableHeaderActions v-if="!isNoneSelected"
|
||||
:current-view="currentView"
|
||||
:selected-nodes="selectedNodes" />
|
||||
|
||||
<!-- Columns display -->
|
||||
<template v-else>
|
||||
<!-- Link to file -->
|
||||
<th class="files-list__column files-list__row-name files-list__column--sortable"
|
||||
@click.stop.prevent="toggleSortBy('basename')">
|
||||
<!-- Icon or preview -->
|
||||
<span class="files-list__row-icon" />
|
||||
|
||||
<!-- Name -->
|
||||
<FilesListTableHeaderButton :name="t('files', 'Name')" mode="basename" />
|
||||
</th>
|
||||
|
||||
<!-- Actions -->
|
||||
<th class="files-list__row-actions" />
|
||||
|
||||
<!-- Size -->
|
||||
<th v-if="isSizeAvailable"
|
||||
:class="{'files-list__column--sortable': isSizeAvailable}"
|
||||
class="files-list__column files-list__row-size">
|
||||
<FilesListTableHeaderButton :name="t('files', 'Size')" mode="size" />
|
||||
</th>
|
||||
|
||||
<!-- Mtime -->
|
||||
<th v-if="isMtimeAvailable"
|
||||
:class="{'files-list__column--sortable': isMtimeAvailable}"
|
||||
class="files-list__column files-list__row-mtime">
|
||||
<FilesListTableHeaderButton :name="t('files', 'Modified')" mode="mtime" />
|
||||
</th>
|
||||
|
||||
<!-- Custom views columns -->
|
||||
<th v-for="column in columns"
|
||||
:key="column.id"
|
||||
:class="classForColumn(column)">
|
||||
<FilesListTableHeaderButton v-if="!!column.sort" :name="column.title" :mode="column.id" />
|
||||
<span v-else>
|
||||
{{ column.title }}
|
||||
</span>
|
||||
</th>
|
||||
</template>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { translate } from '@nextcloud/l10n'
|
||||
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
|
||||
import Vue from 'vue'
|
||||
|
||||
import { useFilesStore } from '../store/files.ts'
|
||||
import { useSelectionStore } from '../store/selection.ts'
|
||||
import FilesListTableHeaderActions from './FilesListTableHeaderActions.vue'
|
||||
import FilesListTableHeaderButton from './FilesListTableHeaderButton.vue'
|
||||
import filesSortingMixin from '../mixins/filesSorting.ts'
|
||||
import logger from '../logger.js'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'FilesListTableHeader',
|
||||
|
||||
components: {
|
||||
FilesListTableHeaderButton,
|
||||
NcCheckboxRadioSwitch,
|
||||
FilesListTableHeaderActions,
|
||||
},
|
||||
|
||||
mixins: [
|
||||
filesSortingMixin,
|
||||
],
|
||||
|
||||
props: {
|
||||
isMtimeAvailable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isSizeAvailable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
nodes: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
filesListWidth: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
|
||||
setup() {
|
||||
const filesStore = useFilesStore()
|
||||
const selectionStore = useSelectionStore()
|
||||
return {
|
||||
filesStore,
|
||||
selectionStore,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
currentView() {
|
||||
return this.$navigation.active
|
||||
},
|
||||
|
||||
columns() {
|
||||
// Hide columns if the list is too small
|
||||
if (this.filesListWidth < 512) {
|
||||
return []
|
||||
}
|
||||
return this.currentView?.columns || []
|
||||
},
|
||||
|
||||
dir() {
|
||||
// Remove any trailing slash but leave root slash
|
||||
return (this.$route?.query?.dir || '/').replace(/^(.+)\/$/, '$1')
|
||||
},
|
||||
|
||||
selectAllBind() {
|
||||
const label = this.isNoneSelected || this.isSomeSelected
|
||||
? this.t('files', 'Select all')
|
||||
: this.t('files', 'Unselect all')
|
||||
return {
|
||||
'aria-label': label,
|
||||
checked: this.isAllSelected,
|
||||
indeterminate: this.isSomeSelected,
|
||||
title: label,
|
||||
}
|
||||
},
|
||||
|
||||
selectedNodes() {
|
||||
return this.selectionStore.selected
|
||||
},
|
||||
|
||||
isAllSelected() {
|
||||
return this.selectedNodes.length === this.nodes.length
|
||||
},
|
||||
|
||||
isNoneSelected() {
|
||||
return this.selectedNodes.length === 0
|
||||
},
|
||||
|
||||
isSomeSelected() {
|
||||
return !this.isAllSelected && !this.isNoneSelected
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
classForColumn(column) {
|
||||
return {
|
||||
'files-list__column': true,
|
||||
'files-list__column--sortable': !!column.sort,
|
||||
'files-list__row-column-custom': true,
|
||||
[`files-list__row-${this.currentView.id}-${column.id}`]: true,
|
||||
}
|
||||
},
|
||||
|
||||
onToggleAll(selected) {
|
||||
if (selected) {
|
||||
const selection = this.nodes.map(node => node.fileid.toString())
|
||||
logger.debug('Added all nodes to selection', { selection })
|
||||
this.selectionStore.setLastIndex(null)
|
||||
this.selectionStore.set(selection)
|
||||
} else {
|
||||
logger.debug('Cleared selection')
|
||||
this.selectionStore.reset()
|
||||
}
|
||||
},
|
||||
|
||||
t: translate,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.files-list__column {
|
||||
user-select: none;
|
||||
// Make sure the cell colors don't apply to column headers
|
||||
color: var(--color-text-maxcontrast) !important;
|
||||
|
||||
&--sortable {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
226
apps/files/src/components/FilesListTableHeaderActions.vue
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
<!--
|
||||
- @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
-
|
||||
- @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
-
|
||||
- @license GNU AGPL version 3 or any later version
|
||||
-
|
||||
- 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/>.
|
||||
-
|
||||
-->
|
||||
<template>
|
||||
<th class="files-list__column files-list__row-actions-batch" colspan="2">
|
||||
<NcActions ref="actionsMenu"
|
||||
:disabled="!!loading || areSomeNodesLoading"
|
||||
:force-name="true"
|
||||
:inline="inlineActions"
|
||||
:menu-name="inlineActions <= 1 ? t('files', 'Actions') : null"
|
||||
:open.sync="openedMenu">
|
||||
<NcActionButton v-for="action in enabledActions"
|
||||
:key="action.id"
|
||||
:class="'files-list__row-actions-batch-' + action.id"
|
||||
@click="onActionClick(action)">
|
||||
<template #icon>
|
||||
<NcLoadingIcon v-if="loading === action.id" :size="18" />
|
||||
<CustomSvgIconRender v-else :svg="action.iconSvgInline(nodes, currentView)" />
|
||||
</template>
|
||||
{{ action.displayName(nodes, currentView) }}
|
||||
</NcActionButton>
|
||||
</NcActions>
|
||||
</th>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { showError, showSuccess } from '@nextcloud/dialogs'
|
||||
import { translate } from '@nextcloud/l10n'
|
||||
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
|
||||
import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
|
||||
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
|
||||
import Vue from 'vue'
|
||||
|
||||
import { getFileActions } from '../services/FileAction.ts'
|
||||
import { useActionsMenuStore } from '../store/actionsmenu.ts'
|
||||
import { useFilesStore } from '../store/files.ts'
|
||||
import { useSelectionStore } from '../store/selection.ts'
|
||||
import filesListWidthMixin from '../mixins/filesListWidth.ts'
|
||||
import CustomSvgIconRender from './CustomSvgIconRender.vue'
|
||||
import logger from '../logger.js'
|
||||
|
||||
// The registered actions list
|
||||
const actions = getFileActions()
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'FilesListTableHeaderActions',
|
||||
|
||||
components: {
|
||||
CustomSvgIconRender,
|
||||
NcActions,
|
||||
NcActionButton,
|
||||
NcLoadingIcon,
|
||||
},
|
||||
|
||||
mixins: [
|
||||
filesListWidthMixin,
|
||||
],
|
||||
|
||||
props: {
|
||||
currentView: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
selectedNodes: {
|
||||
type: Array,
|
||||
default: () => ([]),
|
||||
},
|
||||
},
|
||||
|
||||
setup() {
|
||||
const actionsMenuStore = useActionsMenuStore()
|
||||
const filesStore = useFilesStore()
|
||||
const selectionStore = useSelectionStore()
|
||||
return {
|
||||
actionsMenuStore,
|
||||
filesStore,
|
||||
selectionStore,
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
loading: null,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
dir() {
|
||||
// Remove any trailing slash but leave root slash
|
||||
return (this.$route?.query?.dir || '/').replace(/^(.+)\/$/, '$1')
|
||||
},
|
||||
enabledActions() {
|
||||
return actions
|
||||
.filter(action => action.execBatch)
|
||||
.filter(action => !action.enabled || action.enabled(this.nodes, this.currentView))
|
||||
.sort((a, b) => (a.order || 0) - (b.order || 0))
|
||||
},
|
||||
|
||||
nodes() {
|
||||
return this.selectedNodes
|
||||
.map(fileid => this.getNode(fileid))
|
||||
.filter(node => node)
|
||||
},
|
||||
|
||||
areSomeNodesLoading() {
|
||||
return this.nodes.some(node => node._loading)
|
||||
},
|
||||
|
||||
openedMenu: {
|
||||
get() {
|
||||
return this.actionsMenuStore.opened === 'global'
|
||||
},
|
||||
set(opened) {
|
||||
this.actionsMenuStore.opened = opened ? 'global' : null
|
||||
},
|
||||
},
|
||||
|
||||
inlineActions() {
|
||||
if (this.filesListWidth < 512) {
|
||||
return 0
|
||||
}
|
||||
if (this.filesListWidth < 768) {
|
||||
return 1
|
||||
}
|
||||
if (this.filesListWidth < 1024) {
|
||||
return 2
|
||||
}
|
||||
return 3
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Get a cached note from the store
|
||||
*
|
||||
* @param {number} fileId the file id to get
|
||||
* @return {Folder|File}
|
||||
*/
|
||||
getNode(fileId) {
|
||||
return this.filesStore.getNode(fileId)
|
||||
},
|
||||
|
||||
async onActionClick(action) {
|
||||
const displayName = action.displayName(this.nodes, this.currentView)
|
||||
const selectionIds = this.selectedNodes
|
||||
try {
|
||||
// Set loading markers
|
||||
this.loading = action.id
|
||||
this.nodes.forEach(node => {
|
||||
Vue.set(node, '_loading', true)
|
||||
})
|
||||
|
||||
// Dispatch action execution
|
||||
const results = await action.execBatch(this.nodes, this.currentView, this.dir)
|
||||
|
||||
// Check if all actions returned null
|
||||
if (!results.some(result => result !== null)) {
|
||||
// If the actions returned null, we stay silent
|
||||
this.selectionStore.reset()
|
||||
return
|
||||
}
|
||||
|
||||
// Handle potential failures
|
||||
if (results.some(result => result === false)) {
|
||||
// Remove the failed ids from the selection
|
||||
const failedIds = selectionIds
|
||||
.filter((fileid, index) => results[index] === false)
|
||||
this.selectionStore.set(failedIds)
|
||||
|
||||
showError(this.t('files', '"{displayName}" failed on some elements ', { displayName }))
|
||||
return
|
||||
}
|
||||
|
||||
// Show success message and clear selection
|
||||
showSuccess(this.t('files', '"{displayName}" batch action executed successfully', { displayName }))
|
||||
this.selectionStore.reset()
|
||||
} catch (e) {
|
||||
logger.error('Error while executing action', { action, e })
|
||||
showError(this.t('files', '"{displayName}" action failed', { displayName }))
|
||||
} finally {
|
||||
// Remove loading markers
|
||||
this.loading = null
|
||||
this.nodes.forEach(node => {
|
||||
Vue.set(node, '_loading', false)
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
t: translate,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.files-list__row-actions-batch {
|
||||
flex: 1 1 100% !important;
|
||||
|
||||
// Remove when https://github.com/nextcloud/nextcloud-vue/pull/3936 is merged
|
||||
::v-deep .button-vue__wrapper {
|
||||
width: 100%;
|
||||
span.button-vue__text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
122
apps/files/src/components/FilesListTableHeaderButton.vue
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
<!--
|
||||
- @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
-
|
||||
- @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
-
|
||||
- @license GNU AGPL version 3 or any later version
|
||||
-
|
||||
- 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/>.
|
||||
-
|
||||
-->
|
||||
<template>
|
||||
<NcButton :aria-label="sortAriaLabel(name)"
|
||||
:class="{'files-list__column-sort-button--active': sortingMode === mode}"
|
||||
class="files-list__column-sort-button"
|
||||
type="tertiary"
|
||||
@click.stop.prevent="toggleSortBy(mode)">
|
||||
<!-- Sort icon before text as size is align right -->
|
||||
<MenuUp v-if="sortingMode !== mode || isAscSorting" slot="icon" />
|
||||
<MenuDown v-else slot="icon" />
|
||||
{{ name }}
|
||||
</NcButton>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { translate } from '@nextcloud/l10n'
|
||||
import MenuDown from 'vue-material-design-icons/MenuDown.vue'
|
||||
import MenuUp from 'vue-material-design-icons/MenuUp.vue'
|
||||
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
|
||||
import Vue from 'vue'
|
||||
|
||||
import filesSortingMixin from '../mixins/filesSorting.ts'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'FilesListTableHeaderButton',
|
||||
|
||||
components: {
|
||||
MenuDown,
|
||||
MenuUp,
|
||||
NcButton,
|
||||
},
|
||||
|
||||
mixins: [
|
||||
filesSortingMixin,
|
||||
],
|
||||
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
sortAriaLabel(column) {
|
||||
const direction = this.isAscSorting
|
||||
? this.t('files', 'ascending')
|
||||
: this.t('files', 'descending')
|
||||
return this.t('files', 'Sort list by {column} ({direction})', {
|
||||
column,
|
||||
direction,
|
||||
})
|
||||
},
|
||||
|
||||
t: translate,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.files-list__column-sort-button {
|
||||
// Compensate for cells margin
|
||||
margin: 0 calc(var(--cell-margin) * -1);
|
||||
// Reverse padding
|
||||
padding: 0 4px 0 16px !important;
|
||||
|
||||
// Icon after text
|
||||
.button-vue__wrapper {
|
||||
flex-direction: row-reverse;
|
||||
// Take max inner width for text overflow ellipsis
|
||||
// Remove when https://github.com/nextcloud/nextcloud-vue/pull/3936 is merged
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button-vue__icon {
|
||||
transition-timing-function: linear;
|
||||
transition-duration: .1s;
|
||||
transition-property: opacity;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
// Remove when https://github.com/nextcloud/nextcloud-vue/pull/3936 is merged
|
||||
.button-vue__text {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&--active,
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
.button-vue__icon {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -20,28 +20,18 @@
|
|||
-
|
||||
-->
|
||||
<template>
|
||||
<RecycleScroller ref="recycleScroller"
|
||||
class="files-list"
|
||||
key-field="source"
|
||||
:items="nodes"
|
||||
:item-size="55"
|
||||
:table-mode="true"
|
||||
item-class="files-list__row"
|
||||
item-tag="tr"
|
||||
list-class="files-list__body"
|
||||
list-tag="tbody"
|
||||
role="table">
|
||||
<template #default="{ item, active, index }">
|
||||
<!-- File row -->
|
||||
<FileEntry :active="active"
|
||||
:index="index"
|
||||
:is-mtime-available="isMtimeAvailable"
|
||||
:is-size-available="isSizeAvailable"
|
||||
:files-list-width="filesListWidth"
|
||||
:nodes="nodes"
|
||||
:source="item" />
|
||||
</template>
|
||||
|
||||
<VirtualList :data-component="FileEntry"
|
||||
:data-key="'source'"
|
||||
:data-sources="nodes"
|
||||
:item-height="56"
|
||||
:extra-props="{
|
||||
isMtimeAvailable,
|
||||
isSizeAvailable,
|
||||
nodes,
|
||||
filesListWidth,
|
||||
}"
|
||||
:scroll-to-index="scrollToIndex">
|
||||
<!-- Accessibility description and headers -->
|
||||
<template #before>
|
||||
<!-- Accessibility description -->
|
||||
<caption class="hidden-visually">
|
||||
|
|
@ -49,42 +39,56 @@
|
|||
{{ t('files', 'This list is not fully rendered for performance reasons. The files will be rendered as you navigate through the list.') }}
|
||||
</caption>
|
||||
|
||||
<!-- Thead-->
|
||||
<FilesListHeader :files-list-width="filesListWidth"
|
||||
<!-- Headers -->
|
||||
<FilesListHeader v-for="header in sortedHeaders"
|
||||
:key="header.id"
|
||||
:current-folder="currentFolder"
|
||||
:current-view="currentView"
|
||||
:header="header" />
|
||||
</template>
|
||||
|
||||
<!-- Thead-->
|
||||
<template #header>
|
||||
<FilesListTableHeader :files-list-width="filesListWidth"
|
||||
:is-mtime-available="isMtimeAvailable"
|
||||
:is-size-available="isSizeAvailable"
|
||||
:nodes="nodes" />
|
||||
</template>
|
||||
|
||||
<template #after>
|
||||
<!-- Tfoot-->
|
||||
<FilesListFooter :files-list-width="filesListWidth"
|
||||
<!-- Tfoot-->
|
||||
<template #footer>
|
||||
<FilesListTableFooter :files-list-width="filesListWidth"
|
||||
:is-mtime-available="isMtimeAvailable"
|
||||
:is-size-available="isSizeAvailable"
|
||||
:nodes="nodes"
|
||||
:summary="summary" />
|
||||
</template>
|
||||
</RecycleScroller>
|
||||
</VirtualList>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { RecycleScroller } from 'vue-virtual-scroller'
|
||||
import { translate, translatePlural } from '@nextcloud/l10n'
|
||||
import { getFileListHeaders, type Node } from '@nextcloud/files'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import Vue from 'vue'
|
||||
import VirtualList from './VirtualList.vue'
|
||||
|
||||
import { action as sidebarAction } from '../actions/sidebarAction.ts'
|
||||
import FileEntry from './FileEntry.vue'
|
||||
import FilesListFooter from './FilesListFooter.vue'
|
||||
import FilesListHeader from './FilesListHeader.vue'
|
||||
import FilesListTableFooter from './FilesListTableFooter.vue'
|
||||
import FilesListTableHeader from './FilesListTableHeader.vue'
|
||||
import filesListWidthMixin from '../mixins/filesListWidth.ts'
|
||||
import logger from '../logger.js'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'FilesListVirtual',
|
||||
|
||||
components: {
|
||||
RecycleScroller,
|
||||
FileEntry,
|
||||
FilesListHeader,
|
||||
FilesListFooter,
|
||||
FilesListTableHeader,
|
||||
FilesListTableFooter,
|
||||
VirtualList,
|
||||
},
|
||||
|
||||
mixins: [
|
||||
|
|
@ -96,6 +100,10 @@ export default Vue.extend({
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
currentFolder: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
nodes: {
|
||||
type: Array,
|
||||
required: true,
|
||||
|
|
@ -105,6 +113,8 @@ export default Vue.extend({
|
|||
data() {
|
||||
return {
|
||||
FileEntry,
|
||||
headers: getFileListHeaders(),
|
||||
scrollToIndex: 0,
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -113,6 +123,10 @@ export default Vue.extend({
|
|||
return this.nodes.filter(node => node.type === 'file')
|
||||
},
|
||||
|
||||
fileId() {
|
||||
return parseInt(this.$route.params.fileid || this.$route.query.fileid) || null
|
||||
},
|
||||
|
||||
summaryFile() {
|
||||
const count = this.files.length
|
||||
return translatePlural('files', '{count} file', '{count} files', count, { count })
|
||||
|
|
@ -138,13 +152,36 @@ export default Vue.extend({
|
|||
}
|
||||
return this.nodes.some(node => node.attributes.size !== undefined)
|
||||
},
|
||||
|
||||
sortedHeaders() {
|
||||
if (!this.currentFolder || !this.currentView) {
|
||||
return []
|
||||
}
|
||||
|
||||
return [...this.headers].sort((a, b) => a.order - b.order)
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
// Make the root recycle scroller a table for proper semantics
|
||||
const slots = this.$el.querySelectorAll('.vue-recycle-scroller__slot')
|
||||
slots[0].setAttribute('role', 'thead')
|
||||
slots[1].setAttribute('role', 'tfoot')
|
||||
// Scroll to the file if it's in the url
|
||||
if (this.fileId) {
|
||||
const index = this.nodes.findIndex(node => node.fileid === this.fileId)
|
||||
if (index === -1) {
|
||||
showError(this.t('files', 'File not found'))
|
||||
}
|
||||
this.scrollToIndex = Math.max(0, index)
|
||||
}
|
||||
|
||||
// Open the file sidebar if we have the room for it
|
||||
if (document.documentElement.clientWidth > 1024) {
|
||||
// Open the sidebar on the file if it's in the url and
|
||||
// we're just loaded the app for the first time.
|
||||
const node = this.nodes.find(n => n.fileid === this.fileId) as Node
|
||||
if (node && sidebarAction?.enabled?.([node], this.currentView)) {
|
||||
logger.debug('Opening sidebar on file ' + node.path, { node })
|
||||
sidebarAction.exec(node, this.currentView, this.currentFolder)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
|
@ -173,7 +210,7 @@ export default Vue.extend({
|
|||
|
||||
&::v-deep {
|
||||
// Table head, body and footer
|
||||
tbody, .vue-recycle-scroller__slot {
|
||||
tbody {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
|
@ -181,22 +218,36 @@ export default Vue.extend({
|
|||
position: relative;
|
||||
}
|
||||
|
||||
// Before table and thead
|
||||
.files-list__before {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
// Table header
|
||||
.vue-recycle-scroller__slot[role='thead'] {
|
||||
.files-list__thead {
|
||||
// Pinned on top when scrolling
|
||||
position: sticky;
|
||||
z-index: 10;
|
||||
top: 0;
|
||||
height: var(--row-height);
|
||||
}
|
||||
|
||||
.files-list__thead,
|
||||
.files-list__tfoot {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
background-color: var(--color-main-background);
|
||||
|
||||
}
|
||||
|
||||
tr {
|
||||
position: absolute;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
user-select: none;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
td, th {
|
||||
|
|
@ -221,8 +272,21 @@ export default Vue.extend({
|
|||
}
|
||||
}
|
||||
|
||||
.files-list__row--failed {
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
opacity: .1;
|
||||
z-index: -1;
|
||||
background: var(--color-error);
|
||||
}
|
||||
|
||||
.files-list__row-checkbox {
|
||||
justify-content: center;
|
||||
|
||||
.checkbox-radio-switch {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
|
@ -242,9 +306,14 @@ export default Vue.extend({
|
|||
}
|
||||
}
|
||||
|
||||
// Hover state of the row should also change the favorite markers background
|
||||
.files-list__row:hover .favorite-marker-icon svg path {
|
||||
stroke: var(--color-background-dark);
|
||||
.files-list__row{
|
||||
&:hover, &:focus, &:active, &--active {
|
||||
background-color: var(--color-background-dark);
|
||||
// Hover state of the row should also change the favorite markers background
|
||||
.favorite-marker-icon svg path {
|
||||
stroke: var(--color-background-dark);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Entry preview or mime icon
|
||||
|
|
|
|||
|
|
@ -134,13 +134,13 @@ export default {
|
|||
// User storage stats display
|
||||
.app-navigation-entry__settings-quota {
|
||||
// Align title with progress and icon
|
||||
&--not-unlimited::v-deep .app-navigation-entry__title {
|
||||
margin-top: -4px;
|
||||
&--not-unlimited::v-deep .app-navigation-entry__name {
|
||||
margin-top: -6px;
|
||||
}
|
||||
|
||||
progress {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
bottom: 12px;
|
||||
margin-left: 44px;
|
||||
width: calc(100% - 44px - 22px);
|
||||
}
|
||||
|
|
|
|||
163
apps/files/src/components/VirtualList.vue
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
<template>
|
||||
<table class="files-list" data-cy-files-list>
|
||||
<!-- Header -->
|
||||
<div ref="before" class="files-list__before">
|
||||
<slot name="before" />
|
||||
</div>
|
||||
|
||||
<!-- Header -->
|
||||
<thead ref="thead" class="files-list__thead" data-cy-files-list-thead>
|
||||
<slot name="header" />
|
||||
</thead>
|
||||
|
||||
<!-- Body -->
|
||||
<tbody :style="tbodyStyle" class="files-list__tbody" data-cy-files-list-tbody>
|
||||
<component :is="dataComponent"
|
||||
v-for="(item, i) in renderedItems"
|
||||
:key="i"
|
||||
:visible="(i >= bufferItems || index <= bufferItems) && (i < shownItems - bufferItems)"
|
||||
:source="item"
|
||||
:index="i"
|
||||
v-bind="extraProps" />
|
||||
</tbody>
|
||||
|
||||
<!-- Footer -->
|
||||
<tfoot v-show="isReady"
|
||||
ref="tfoot"
|
||||
class="files-list__tfoot"
|
||||
data-cy-files-list-tfoot>
|
||||
<slot name="footer" />
|
||||
</tfoot>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { File, Folder } from '@nextcloud/files'
|
||||
import { debounce } from 'debounce'
|
||||
import Vue from 'vue'
|
||||
import logger from '../logger.js'
|
||||
|
||||
// Items to render before and after the visible area
|
||||
const bufferItems = 3
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'VirtualList',
|
||||
|
||||
props: {
|
||||
dataComponent: {
|
||||
type: [Object, Function],
|
||||
required: true,
|
||||
},
|
||||
dataKey: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
dataSources: {
|
||||
type: Array as () => (File | Folder)[],
|
||||
required: true,
|
||||
},
|
||||
itemHeight: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
extraProps: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
scrollToIndex: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
bufferItems,
|
||||
index: this.scrollToIndex,
|
||||
beforeHeight: 0,
|
||||
headerHeight: 0,
|
||||
tableHeight: 0,
|
||||
resizeObserver: null as ResizeObserver | null,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
// Wait for measurements to be done before rendering
|
||||
isReady() {
|
||||
return this.tableHeight > 0
|
||||
},
|
||||
|
||||
startIndex() {
|
||||
return Math.max(0, this.index - bufferItems)
|
||||
},
|
||||
shownItems() {
|
||||
return Math.ceil((this.tableHeight - this.headerHeight) / this.itemHeight) + bufferItems * 2
|
||||
},
|
||||
renderedItems(): (File | Folder)[] {
|
||||
if (!this.isReady) {
|
||||
return []
|
||||
}
|
||||
return this.dataSources.slice(this.startIndex, this.startIndex + this.shownItems)
|
||||
},
|
||||
|
||||
tbodyStyle() {
|
||||
const isOverScrolled = this.startIndex + this.shownItems > this.dataSources.length
|
||||
const lastIndex = this.dataSources.length - this.startIndex - this.shownItems
|
||||
const hiddenAfterItems = Math.min(this.dataSources.length - this.startIndex, lastIndex)
|
||||
return {
|
||||
paddingTop: `${this.startIndex * this.itemHeight}px`,
|
||||
paddingBottom: isOverScrolled ? 0 : `${hiddenAfterItems * this.itemHeight}px`,
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
scrollToIndex() {
|
||||
this.index = this.scrollToIndex
|
||||
this.$el.scrollTop = this.index * this.itemHeight + this.beforeHeight
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
const before = this.$refs?.before as HTMLElement
|
||||
const root = this.$el as HTMLElement
|
||||
const tfoot = this.$refs?.tfoot as HTMLElement
|
||||
const thead = this.$refs?.thead as HTMLElement
|
||||
|
||||
this.resizeObserver = new ResizeObserver(debounce(() => {
|
||||
this.beforeHeight = before?.clientHeight ?? 0
|
||||
this.headerHeight = thead?.clientHeight ?? 0
|
||||
this.tableHeight = root?.clientHeight ?? 0
|
||||
logger.debug('VirtualList resizeObserver updated')
|
||||
this.onScroll()
|
||||
}, 100, false))
|
||||
|
||||
this.resizeObserver.observe(before)
|
||||
this.resizeObserver.observe(root)
|
||||
this.resizeObserver.observe(tfoot)
|
||||
this.resizeObserver.observe(thead)
|
||||
|
||||
this.$el.addEventListener('scroll', this.onScroll)
|
||||
|
||||
if (this.scrollToIndex) {
|
||||
this.$el.scrollTop = this.index * this.itemHeight + this.beforeHeight
|
||||
}
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
if (this.resizeObserver) {
|
||||
this.resizeObserver.disconnect()
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onScroll() {
|
||||
// Max 0 to prevent negative index
|
||||
this.index = Math.max(0, Math.round((this.$el.scrollTop - this.beforeHeight) / this.itemHeight))
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
/**
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import logger from '../logger.js'
|
||||
|
||||
/**
|
||||
* Fetch and register the legacy files views
|
||||
*/
|
||||
export default function() {
|
||||
const legacyViews = Object.values(loadState('files', 'navigation', {}))
|
||||
|
||||
if (legacyViews.length > 0) {
|
||||
logger.debug('Legacy files views detected. Processing...', legacyViews)
|
||||
legacyViews.forEach(view => {
|
||||
registerLegacyView(view)
|
||||
if (view.sublist) {
|
||||
view.sublist.forEach(subview => registerLegacyView({ ...subview, parent: view.id }))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const registerLegacyView = function({ id, name, order, icon, parent, classes = '', expanded, params }) {
|
||||
OCP.Files.Navigation.register({
|
||||
id,
|
||||
name,
|
||||
order,
|
||||
params,
|
||||
parent,
|
||||
expanded: expanded === true,
|
||||
iconClass: icon ? `icon-${icon}` : 'nav-icon-' + id,
|
||||
legacy: true,
|
||||
sticky: classes.includes('pinned'),
|
||||
})
|
||||
}
|
||||
|
|
@ -15,13 +15,13 @@ import Vue from 'vue'
|
|||
import { createPinia, PiniaVuePlugin } from 'pinia'
|
||||
|
||||
import FilesListView from './views/FilesList.vue'
|
||||
import NavigationService from './services/Navigation'
|
||||
import { NavigationService } from './services/Navigation'
|
||||
import NavigationView from './views/Navigation.vue'
|
||||
import processLegacyFilesViews from './legacy/navigationMapper.js'
|
||||
import registerFavoritesView from './views/favorites'
|
||||
import registerRecentView from './views/recent'
|
||||
import registerFilesView from './views/files'
|
||||
import registerPreviewServiceWorker from './services/ServiceWorker.js'
|
||||
import router from './router/router.js'
|
||||
import router from './router/router'
|
||||
import RouterService from './services/RouterService'
|
||||
import SettingsModel from './models/Setting.js'
|
||||
import SettingsService from './services/Settings.js'
|
||||
|
|
@ -78,8 +78,8 @@ const FilesList = new ListView({
|
|||
FilesList.$mount('#app-content-vue')
|
||||
|
||||
// Init legacy and new files views
|
||||
processLegacyFilesViews()
|
||||
registerFavoritesView()
|
||||
registerFilesView()
|
||||
registerRecentView()
|
||||
|
||||
// Register preview service worker
|
||||
|
|
|
|||
|
|
@ -23,14 +23,14 @@ import Vue from 'vue'
|
|||
|
||||
import { mapState } from 'pinia'
|
||||
import { useViewConfigStore } from '../store/viewConfig'
|
||||
import type { Navigation } from '../services/Navigation'
|
||||
import type { NavigationService, Navigation } from '../services/Navigation'
|
||||
|
||||
export default Vue.extend({
|
||||
computed: {
|
||||
...mapState(useViewConfigStore, ['getConfig', 'setSortingBy', 'toggleSortingDirection']),
|
||||
|
||||
currentView(): Navigation {
|
||||
return this.$navigation.active
|
||||
return (this.$navigation as NavigationService).active as Navigation
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -19,26 +19,34 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
import Vue from 'vue'
|
||||
import Router from 'vue-router'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import queryString from 'query-string'
|
||||
import Router, { RawLocation, Route } from 'vue-router'
|
||||
import Vue from 'vue'
|
||||
import { ErrorHandler } from 'vue-router/types/router'
|
||||
|
||||
Vue.use(Router)
|
||||
|
||||
// Prevent router from throwing errors when we're already on the page we're trying to go to
|
||||
const originalPush = Router.prototype.push as (to, onComplete?, onAbort?) => Promise<Route>
|
||||
Router.prototype.push = function push(to: RawLocation, onComplete?: ((route: Route) => void) | undefined, onAbort?: ErrorHandler | undefined): Promise<Route> {
|
||||
if (onComplete || onAbort) return originalPush.call(this, to, onComplete, onAbort)
|
||||
return originalPush.call(this, to).catch(err => err)
|
||||
}
|
||||
|
||||
const router = new Router({
|
||||
mode: 'history',
|
||||
|
||||
// if index.php is in the url AND we got this far, then it's working:
|
||||
// let's keep using index.php in the url
|
||||
base: generateUrl('/apps/files', ''),
|
||||
base: generateUrl('/apps/files'),
|
||||
linkActiveClass: 'active',
|
||||
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
// Pretending we're using the default view
|
||||
alias: '/files',
|
||||
redirect: { name: 'filelist' },
|
||||
},
|
||||
{
|
||||
path: '/:view/:fileid?',
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
import { File, Folder, parseWebdavPermissions } from '@nextcloud/files'
|
||||
import { File, Folder, davParsePermissions } from '@nextcloud/files'
|
||||
import { generateRemoteUrl } from '@nextcloud/router'
|
||||
import { getClient, rootPath } from './WebdavClient'
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
|
|
@ -47,7 +47,7 @@ interface ResponseProps extends DAVResultResponseProps {
|
|||
|
||||
const resultToNode = function(node: FileStat): File | Folder {
|
||||
const props = node.props as ResponseProps
|
||||
const permissions = parseWebdavPermissions(props?.permissions)
|
||||
const permissions = davParsePermissions(props?.permissions)
|
||||
const owner = getCurrentUser()?.uid as string
|
||||
|
||||
const nodeData = {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
*/
|
||||
|
||||
import axios from '@nextcloud/axios'
|
||||
import { davGetDefaultPropfind } from '@nextcloud/files'
|
||||
|
||||
/**
|
||||
* @param {any} url -
|
||||
|
|
@ -29,33 +30,7 @@ export default async function(url) {
|
|||
const response = await axios({
|
||||
method: 'PROPFIND',
|
||||
url,
|
||||
data: `<?xml version="1.0"?>
|
||||
<d:propfind xmlns:d="DAV:"
|
||||
xmlns:oc="http://owncloud.org/ns"
|
||||
xmlns:nc="http://nextcloud.org/ns"
|
||||
xmlns:ocs="http://open-collaboration-services.org/ns">
|
||||
<d:prop>
|
||||
<d:getlastmodified />
|
||||
<d:getetag />
|
||||
<d:getcontenttype />
|
||||
<d:resourcetype />
|
||||
<oc:fileid />
|
||||
<oc:permissions />
|
||||
<oc:size />
|
||||
<d:getcontentlength />
|
||||
<nc:has-preview />
|
||||
<nc:mount-type />
|
||||
<nc:is-encrypted />
|
||||
<ocs:share-permissions />
|
||||
<nc:share-attributes />
|
||||
<oc:tags />
|
||||
<oc:favorite />
|
||||
<oc:comments-unread />
|
||||
<oc:owner-id />
|
||||
<oc:owner-display-name />
|
||||
<oc:share-types />
|
||||
</d:prop>
|
||||
</d:propfind>`,
|
||||
data: davGetDefaultPropfind(),
|
||||
})
|
||||
|
||||
// TODO: create new parser or use cdav-lib when available
|
||||
|
|
|
|||
102
apps/files/src/services/Files.ts
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
/**
|
||||
* @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 { ContentsWithRoot } from './Navigation'
|
||||
import type { FileStat, ResponseDataDetailed, DAVResultResponseProps } from 'webdav'
|
||||
|
||||
import { File, Folder, davParsePermissions } from '@nextcloud/files'
|
||||
import { generateRemoteUrl } from '@nextcloud/router'
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
|
||||
import { getClient, rootPath } from './WebdavClient'
|
||||
import { getDefaultPropfind } from './DavProperties'
|
||||
import { hashCode } from '../utils/hashUtils'
|
||||
import logger from '../logger'
|
||||
|
||||
const client = getClient()
|
||||
|
||||
interface ResponseProps extends DAVResultResponseProps {
|
||||
permissions: string,
|
||||
fileid: number,
|
||||
size: number,
|
||||
}
|
||||
|
||||
const resultToNode = function(node: FileStat): File | Folder {
|
||||
const props = node.props as ResponseProps
|
||||
const permissions = davParsePermissions(props?.permissions)
|
||||
const owner = getCurrentUser()?.uid as string
|
||||
|
||||
const source = generateRemoteUrl('dav' + rootPath + node.filename)
|
||||
const id = props?.fileid < 0
|
||||
? hashCode(source)
|
||||
: props?.fileid as number || 0
|
||||
|
||||
const nodeData = {
|
||||
id,
|
||||
source,
|
||||
mtime: new Date(node.lastmod),
|
||||
mime: node.mime as string,
|
||||
size: props?.size as number || 0,
|
||||
permissions,
|
||||
owner,
|
||||
root: rootPath,
|
||||
attributes: {
|
||||
...node,
|
||||
...props,
|
||||
hasPreview: props?.['has-preview'],
|
||||
failed: props?.fileid < 0,
|
||||
},
|
||||
}
|
||||
|
||||
delete nodeData.attributes.props
|
||||
|
||||
return node.type === 'file'
|
||||
? new File(nodeData)
|
||||
: new Folder(nodeData)
|
||||
}
|
||||
|
||||
export const getContents = async (path = '/'): Promise<ContentsWithRoot> => {
|
||||
const propfindPayload = getDefaultPropfind()
|
||||
|
||||
const contentsResponse = await client.getDirectoryContents(path, {
|
||||
details: true,
|
||||
data: propfindPayload,
|
||||
includeSelf: true,
|
||||
}) as ResponseDataDetailed<FileStat[]>
|
||||
|
||||
const root = contentsResponse.data[0]
|
||||
const contents = contentsResponse.data.slice(1)
|
||||
if (root.filename !== path) {
|
||||
throw new Error('Root node does not match requested path')
|
||||
}
|
||||
|
||||
return {
|
||||
folder: resultToNode(root) as Folder,
|
||||
contents: contents.map(result => {
|
||||
try {
|
||||
return resultToNode(result)
|
||||
} catch (error) {
|
||||
logger.error(`Invalid node detected '${result.basename}'`, { error })
|
||||
return null
|
||||
}
|
||||
}).filter(Boolean) as File[],
|
||||
}
|
||||
}
|
||||
|
|
@ -96,22 +96,9 @@ export interface Navigation {
|
|||
* haven't customized their sorting column
|
||||
*/
|
||||
defaultSortKey?: string
|
||||
|
||||
/**
|
||||
* This view is sticky a legacy view.
|
||||
* Here until all the views are migrated to Vue.
|
||||
* @deprecated It will be removed in a near future
|
||||
*/
|
||||
legacy?: boolean
|
||||
|
||||
/**
|
||||
* An icon class.
|
||||
* @deprecated It will be removed in a near future
|
||||
*/
|
||||
iconClass?: string
|
||||
}
|
||||
|
||||
export default class {
|
||||
export class NavigationService {
|
||||
|
||||
private _views: Navigation[] = []
|
||||
private _currentView: Navigation | null = null
|
||||
|
|
@ -131,14 +118,6 @@ export default class {
|
|||
throw e
|
||||
}
|
||||
|
||||
if (view.legacy) {
|
||||
logger.warn('Legacy view detected, please migrate to Vue')
|
||||
}
|
||||
|
||||
if (view.iconClass) {
|
||||
view.legacy = true
|
||||
}
|
||||
|
||||
this._views.push(view)
|
||||
}
|
||||
|
||||
|
|
@ -192,18 +171,12 @@ const isValidNavigation = function(view: Navigation): boolean {
|
|||
throw new Error('Navigation caption is required for top-level views and must be a string')
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy handle their content and icon differently
|
||||
* TODO: remove when support for legacy views is removed
|
||||
*/
|
||||
if (!view.legacy) {
|
||||
if (!view.getContents || typeof view.getContents !== 'function') {
|
||||
throw new Error('Navigation getContents is required and must be a function')
|
||||
}
|
||||
if (!view.getContents || typeof view.getContents !== 'function') {
|
||||
throw new Error('Navigation getContents is required and must be a function')
|
||||
}
|
||||
|
||||
if (!view.icon || typeof view.icon !== 'string' || !isSvg(view.icon)) {
|
||||
throw new Error('Navigation icon is required and must be a valid svg string')
|
||||
}
|
||||
if (!view.icon || typeof view.icon !== 'string' || !isSvg(view.icon)) {
|
||||
throw new Error('Navigation icon is required and must be a valid svg string')
|
||||
}
|
||||
|
||||
if (!('order' in view) || typeof view.order !== 'number') {
|
||||
|
|
|
|||
|
|
@ -26,8 +26,12 @@ const SWCacheName = 'previews'
|
|||
/**
|
||||
* Check if the preview is already cached by the service worker
|
||||
*/
|
||||
export const isCachedPreview = function(previewUrl: string) {
|
||||
return caches.open(SWCacheName)
|
||||
export const isCachedPreview = function(previewUrl: string): Promise<boolean> {
|
||||
if (!window?.caches?.open) {
|
||||
return Promise.resolve(false)
|
||||
}
|
||||
|
||||
return window?.caches?.open(SWCacheName)
|
||||
.then(function(cache) {
|
||||
return cache.match(previewUrl)
|
||||
.then(function(response) {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
import { File, Folder, Permission, parseWebdavPermissions } from '@nextcloud/files'
|
||||
import { File, Folder, Permission, davParsePermissions } from '@nextcloud/files'
|
||||
import { generateRemoteUrl } from '@nextcloud/router'
|
||||
import { getClient, rootPath } from './WebdavClient'
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
|
|
@ -94,7 +94,7 @@ interface ResponseProps extends DAVResultResponseProps {
|
|||
|
||||
const resultToNode = function(node: FileStat): File | Folder {
|
||||
const props = node.props as ResponseProps
|
||||
const permissions = parseWebdavPermissions(props?.permissions)
|
||||
const permissions = davParsePermissions(props?.permissions)
|
||||
const owner = getCurrentUser()?.uid as string
|
||||
|
||||
const nodeData = {
|
||||
|
|
|
|||
|
|
@ -31,6 +31,18 @@ export default class RouterService {
|
|||
this._router = router
|
||||
}
|
||||
|
||||
get name(): string | null | undefined {
|
||||
return this._router.currentRoute.name
|
||||
}
|
||||
|
||||
get query(): Dictionary<string | (string | null)[] | null | undefined> {
|
||||
return this._router.currentRoute.query || {}
|
||||
}
|
||||
|
||||
get params(): Dictionary<string> {
|
||||
return this._router.currentRoute.params || {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a route change on the files app
|
||||
*
|
||||
|
|
|
|||
|
|
@ -20,9 +20,7 @@
|
|||
-
|
||||
-->
|
||||
<template>
|
||||
<NcAppContent v-show="!currentView?.legacy"
|
||||
:class="{'app-content--hidden': currentView?.legacy}"
|
||||
data-cy-files-content>
|
||||
<NcAppContent data-cy-files-content>
|
||||
<div class="files-list__header">
|
||||
<!-- Current folder breadcrumbs -->
|
||||
<BreadCrumbs :path="dir" @reload="fetchContent" />
|
||||
|
|
@ -58,19 +56,25 @@
|
|||
<!-- File list -->
|
||||
<FilesListVirtual v-else
|
||||
ref="filesListVirtual"
|
||||
:current-folder="currentFolder"
|
||||
:current-view="currentView"
|
||||
:nodes="dirContents" />
|
||||
:nodes="dirContentsSorted" />
|
||||
</NcAppContent>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Folder, File, Node } from '@nextcloud/files'
|
||||
import type { Route } from 'vue-router'
|
||||
import type { Navigation, ContentsWithRoot } from '../services/Navigation.ts'
|
||||
import type { UserConfig } from '../types.ts'
|
||||
|
||||
import { Folder, Node } from '@nextcloud/files'
|
||||
import { join } from 'path'
|
||||
import { orderBy } from 'natural-orderby'
|
||||
import { translate } from '@nextcloud/l10n'
|
||||
import NcAppContent from '@nextcloud/vue/dist/Components/NcAppContent.js'
|
||||
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
|
||||
import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
|
||||
import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
|
||||
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
|
||||
import Vue from 'vue'
|
||||
|
||||
|
|
@ -83,8 +87,6 @@ import BreadCrumbs from '../components/BreadCrumbs.vue'
|
|||
import FilesListVirtual from '../components/FilesListVirtual.vue'
|
||||
import filesSortingMixin from '../mixins/filesSorting.ts'
|
||||
import logger from '../logger.js'
|
||||
import Navigation, { ContentsWithRoot } from '../services/Navigation.ts'
|
||||
import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'FilesList',
|
||||
|
|
@ -126,32 +128,27 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
computed: {
|
||||
userConfig() {
|
||||
userConfig(): UserConfig {
|
||||
return this.userConfigStore.userConfig
|
||||
},
|
||||
|
||||
/** @return {Navigation} */
|
||||
currentView() {
|
||||
return this.$navigation.active
|
||||
|| this.$navigation.views.find(view => view.id === 'files')
|
||||
currentView(): Navigation {
|
||||
return (this.$navigation.active
|
||||
|| this.$navigation.views.find(view => view.id === 'files')) as Navigation
|
||||
},
|
||||
|
||||
/**
|
||||
* The current directory query.
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
dir() {
|
||||
dir(): string {
|
||||
// Remove any trailing slash but leave root slash
|
||||
return (this.$route?.query?.dir || '/').replace(/^(.+)\/$/, '$1')
|
||||
},
|
||||
|
||||
/**
|
||||
* The current folder.
|
||||
*
|
||||
* @return {Folder|undefined}
|
||||
*/
|
||||
currentFolder() {
|
||||
currentFolder(): Folder|undefined {
|
||||
if (!this.currentView?.id) {
|
||||
return
|
||||
}
|
||||
|
|
@ -165,10 +162,8 @@ export default Vue.extend({
|
|||
|
||||
/**
|
||||
* The current directory contents.
|
||||
*
|
||||
* @return {Node[]}
|
||||
*/
|
||||
dirContents() {
|
||||
dirContentsSorted(): Node[] {
|
||||
if (!this.currentView) {
|
||||
return []
|
||||
}
|
||||
|
|
@ -178,8 +173,7 @@ export default Vue.extend({
|
|||
|
||||
// Custom column must provide their own sorting methods
|
||||
if (customColumn?.sort && typeof customColumn.sort === 'function') {
|
||||
const results = [...(this.currentFolder?._children || []).map(this.getNode).filter(file => file)]
|
||||
.sort(customColumn.sort)
|
||||
const results = [...this.dirContents].sort(customColumn.sort)
|
||||
return this.isAscSorting ? results : results.reverse()
|
||||
}
|
||||
|
||||
|
|
@ -198,16 +192,20 @@ export default Vue.extend({
|
|||
const orders = new Array(identifiers.length).fill(this.isAscSorting ? 'asc' : 'desc')
|
||||
|
||||
return orderBy(
|
||||
[...(this.currentFolder?._children || []).map(this.getNode).filter(file => file)],
|
||||
[...this.dirContents],
|
||||
identifiers,
|
||||
orders,
|
||||
)
|
||||
},
|
||||
|
||||
dirContents(): Node[] {
|
||||
return (this.currentFolder?._children || []).map(this.getNode).filter(file => file)
|
||||
},
|
||||
|
||||
/**
|
||||
* The current directory is empty.
|
||||
*/
|
||||
isEmptyDir() {
|
||||
isEmptyDir(): boolean {
|
||||
return this.dirContents.length === 0
|
||||
},
|
||||
|
||||
|
|
@ -216,7 +214,7 @@ export default Vue.extend({
|
|||
* But we already have a cached version of it
|
||||
* that is not empty.
|
||||
*/
|
||||
isRefreshing() {
|
||||
isRefreshing(): boolean {
|
||||
return this.currentFolder !== undefined
|
||||
&& !this.isEmptyDir
|
||||
&& this.loading
|
||||
|
|
@ -225,7 +223,7 @@ export default Vue.extend({
|
|||
/**
|
||||
* Route to the previous directory.
|
||||
*/
|
||||
toPreviousDir() {
|
||||
toPreviousDir(): Route {
|
||||
const dir = this.dir.split('/').slice(0, -1).join('/') || '/'
|
||||
return { ...this.$route, query: { dir } }
|
||||
},
|
||||
|
|
@ -257,10 +255,6 @@ export default Vue.extend({
|
|||
|
||||
methods: {
|
||||
async fetchContent() {
|
||||
if (this.currentView?.legacy) {
|
||||
return
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
const dir = this.dir
|
||||
const currentView = this.currentView
|
||||
|
|
@ -272,8 +266,7 @@ export default Vue.extend({
|
|||
}
|
||||
|
||||
// Fetch the current dir contents
|
||||
/** @type {Promise<ContentsWithRoot>} */
|
||||
this.promise = currentView.getContents(dir)
|
||||
this.promise = currentView.getContents(dir) as Promise<ContentsWithRoot>
|
||||
try {
|
||||
const { folder, contents } = await this.promise
|
||||
logger.debug('Fetched contents', { dir, folder, contents })
|
||||
|
|
@ -333,12 +326,6 @@ export default Vue.extend({
|
|||
overflow: hidden;
|
||||
flex-direction: column;
|
||||
max-height: 100%;
|
||||
|
||||
// TODO: remove after all legacy views are migrated
|
||||
// Hides the legacy app-content if shown view is not legacy
|
||||
&:not(&--hidden)::v-deep + #app-content {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
$margin: 4px;
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ import FolderSvg from '@mdi/svg/svg/folder.svg'
|
|||
import ShareSvg from '@mdi/svg/svg/share-variant.svg'
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
|
||||
import NavigationService from '../services/Navigation'
|
||||
import { NavigationService } from '../services/Navigation'
|
||||
import NavigationView from './Navigation.vue'
|
||||
import router from '../router/router.js'
|
||||
import router from '../router/router'
|
||||
import { useViewConfigStore } from '../store/viewConfig'
|
||||
|
||||
describe('Navigation renders', () => {
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@
|
|||
</NcAppNavigation>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { emit, subscribe } from '@nextcloud/event-bus'
|
||||
import { translate } from '@nextcloud/l10n'
|
||||
import Cog from 'vue-material-design-icons/Cog.vue'
|
||||
|
|
@ -83,7 +83,7 @@ import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js
|
|||
import { setPageHeading } from '../../../../core/src/OCP/accessibility.js'
|
||||
import { useViewConfigStore } from '../store/viewConfig.ts'
|
||||
import logger from '../logger.js'
|
||||
import Navigation from '../services/Navigation.ts'
|
||||
import type { NavigationService, Navigation } from '../services/Navigation.ts'
|
||||
import NavigationQuota from '../components/NavigationQuota.vue'
|
||||
import SettingsModal from './Settings.vue'
|
||||
|
||||
|
|
@ -102,7 +102,7 @@ export default {
|
|||
props: {
|
||||
// eslint-disable-next-line vue/prop-name-casing
|
||||
Navigation: {
|
||||
type: Navigation,
|
||||
type: Object as Navigation,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
|
@ -125,18 +125,15 @@ export default {
|
|||
return this.$route?.params?.view || 'files'
|
||||
},
|
||||
|
||||
/** @return {Navigation} */
|
||||
currentView() {
|
||||
currentView(): Navigation {
|
||||
return this.views.find(view => view.id === this.currentViewId)
|
||||
},
|
||||
|
||||
/** @return {Navigation[]} */
|
||||
views() {
|
||||
views(): Navigation[] {
|
||||
return this.Navigation.views
|
||||
},
|
||||
|
||||
/** @return {Navigation[]} */
|
||||
parentViews() {
|
||||
parentViews(): Navigation[] {
|
||||
return this.views
|
||||
// filter child views
|
||||
.filter(view => !view.parent)
|
||||
|
|
@ -146,8 +143,7 @@ export default {
|
|||
})
|
||||
},
|
||||
|
||||
/** @return {Navigation[]} */
|
||||
childViews() {
|
||||
childViews(): Navigation[] {
|
||||
return this.views
|
||||
// filter parent views
|
||||
.filter(view => !!view.parent)
|
||||
|
|
@ -165,17 +161,12 @@ export default {
|
|||
|
||||
watch: {
|
||||
currentView(view, oldView) {
|
||||
// If undefined, it means we're initializing the view
|
||||
// This is handled by the legacy-view:initialized event
|
||||
// TODO: remove when legacy views are dropped
|
||||
if (view?.id === oldView?.id) {
|
||||
return
|
||||
if (view.id !== oldView?.id) {
|
||||
this.Navigation.setActive(view)
|
||||
logger.debug('Navigation changed', { id: view.id, view })
|
||||
|
||||
this.showView(view, oldView)
|
||||
}
|
||||
|
||||
this.Navigation.setActive(view)
|
||||
logger.debug('Navigation changed', { id: view.id, view })
|
||||
|
||||
this.showView(view, oldView)
|
||||
},
|
||||
},
|
||||
|
||||
|
|
@ -184,70 +175,22 @@ export default {
|
|||
logger.debug('Navigation mounted. Showing requested view', { view: this.currentView })
|
||||
this.showView(this.currentView)
|
||||
}
|
||||
|
||||
subscribe('files:legacy-navigation:changed', this.onLegacyNavigationChanged)
|
||||
|
||||
// TODO: remove this once the legacy navigation is gone
|
||||
subscribe('files:legacy-view:initialized', () => {
|
||||
logger.debug('Legacy view initialized', { ...this.currentView })
|
||||
this.showView(this.currentView)
|
||||
})
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* @param {Navigation} view the new active view
|
||||
* @param {Navigation} oldView the old active view
|
||||
*/
|
||||
showView(view, oldView) {
|
||||
showView(view: Navigation) {
|
||||
// Closing any opened sidebar
|
||||
window?.OCA?.Files?.Sidebar?.close?.()
|
||||
|
||||
if (view?.legacy) {
|
||||
const newAppContent = document.querySelector('#app-content #app-content-' + this.currentView.id + '.viewcontainer')
|
||||
document.querySelectorAll('#app-content .viewcontainer').forEach(el => {
|
||||
el.classList.add('hidden')
|
||||
})
|
||||
newAppContent.classList.remove('hidden')
|
||||
|
||||
// Triggering legacy navigation events
|
||||
const { dir = '/' } = OC.Util.History.parseUrlQuery()
|
||||
const params = { itemId: view.id, dir }
|
||||
|
||||
logger.debug('Triggering legacy navigation event', params)
|
||||
window.jQuery(newAppContent).trigger(new window.jQuery.Event('show', params))
|
||||
window.jQuery(newAppContent).trigger(new window.jQuery.Event('urlChanged', params))
|
||||
}
|
||||
|
||||
this.Navigation.setActive(view)
|
||||
setPageHeading(view.name)
|
||||
emit('files:navigation:changed', view)
|
||||
},
|
||||
|
||||
/**
|
||||
* Coming from the legacy files app.
|
||||
* TODO: remove when all views are migrated.
|
||||
*
|
||||
* @param {Navigation} view the new active view
|
||||
*/
|
||||
onLegacyNavigationChanged({ id } = { id: 'files' }) {
|
||||
const view = this.Navigation.views.find(view => view.id === id)
|
||||
if (view && view.legacy && view.id !== this.currentView.id) {
|
||||
// Force update the current route as the request comes
|
||||
// from the legacy files app router
|
||||
this.$router.replace({ ...this.$route, params: { view: view.id } })
|
||||
this.Navigation.setActive(view)
|
||||
this.showView(view)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Expand/collapse a a view with children and permanently
|
||||
* save this setting in the server.
|
||||
*
|
||||
* @param {Navigation} view the view to toggle
|
||||
*/
|
||||
onToggleExpand(view) {
|
||||
onToggleExpand(view: Navigation) {
|
||||
// Invert state
|
||||
const isExpanded = this.isExpanded(view)
|
||||
// Update the view expanded state, might not be necessary
|
||||
|
|
@ -258,10 +201,8 @@ export default {
|
|||
/**
|
||||
* Check if a view is expanded by user config
|
||||
* or fallback to the default value.
|
||||
*
|
||||
* @param {Navigation} view the view to check
|
||||
*/
|
||||
isExpanded(view) {
|
||||
isExpanded(view: Navigation): boolean {
|
||||
return typeof this.viewConfigStore.getConfig(view.id)?.expanded === 'boolean'
|
||||
? this.viewConfigStore.getConfig(view.id).expanded === true
|
||||
: view.expanded === true
|
||||
|
|
@ -269,10 +210,8 @@ export default {
|
|||
|
||||
/**
|
||||
* Generate the route to a view
|
||||
*
|
||||
* @param {Navigation} view the view to toggle
|
||||
*/
|
||||
generateToNavigation(view) {
|
||||
generateToNavigation(view: Navigation) {
|
||||
if (view.params) {
|
||||
const { dir, fileid } = view.params
|
||||
return { name: 'filelist', params: view.params, query: { dir, fileid } }
|
||||
|
|
|
|||
|
|
@ -88,20 +88,22 @@
|
|||
</NcAppSidebar>
|
||||
</template>
|
||||
<script>
|
||||
import { emit } from '@nextcloud/event-bus'
|
||||
import { encodePath } from '@nextcloud/paths'
|
||||
import { File, Folder } from '@nextcloud/files'
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
import { Type as ShareTypes } from '@nextcloud/sharing'
|
||||
import $ from 'jquery'
|
||||
import axios from '@nextcloud/axios'
|
||||
import { emit } from '@nextcloud/event-bus'
|
||||
import moment from '@nextcloud/moment'
|
||||
import { Type as ShareTypes } from '@nextcloud/sharing'
|
||||
|
||||
import NcAppSidebar from '@nextcloud/vue/dist/Components/NcAppSidebar.js'
|
||||
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
|
||||
import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
|
||||
|
||||
import FileInfo from '../services/FileInfo.js'
|
||||
import SidebarTab from '../components/SidebarTab.vue'
|
||||
import LegacyView from '../components/LegacyView.vue'
|
||||
import SidebarTab from '../components/SidebarTab.vue'
|
||||
import SystemTags from '../../../systemtags/src/components/SystemTags.vue'
|
||||
|
||||
export default {
|
||||
|
|
@ -253,7 +255,7 @@ export default {
|
|||
return {
|
||||
key: 'error', // force key to re-render
|
||||
subname: '',
|
||||
title: '',
|
||||
name: '',
|
||||
class: {
|
||||
'app-sidebar--full': this.isFullScreen,
|
||||
},
|
||||
|
|
@ -263,7 +265,7 @@ export default {
|
|||
return {
|
||||
loading: this.loading,
|
||||
subname: '',
|
||||
title: '',
|
||||
name: '',
|
||||
class: {
|
||||
'app-sidebar--full': this.isFullScreen,
|
||||
},
|
||||
|
|
@ -372,7 +374,13 @@ export default {
|
|||
*/
|
||||
setActiveTab(id) {
|
||||
OCA.Files.Sidebar.setActiveTab(id)
|
||||
this.tabs.forEach(tab => tab.setIsActive(id === tab.id))
|
||||
this.tabs.forEach(tab => {
|
||||
try {
|
||||
tab.setIsActive(id === tab.id)
|
||||
} catch (error) {
|
||||
logger.error('Error while setting tab active state', { error, id: tab.id, tab })
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -397,12 +405,18 @@ export default {
|
|||
</d:propertyupdate>`,
|
||||
})
|
||||
|
||||
// TODO: Obliterate as soon as possible and use events with new files app
|
||||
// Terrible fallback for legacy files: toggle filelist as well
|
||||
if (OCA.Files && OCA.Files.App && OCA.Files.App.fileList && OCA.Files.App.fileList.fileActions) {
|
||||
OCA.Files.App.fileList.fileActions.triggerAction('Favorite', OCA.Files.App.fileList.getModelForFile(this.fileInfo.name), OCA.Files.App.fileList)
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: adjust this when the Sidebar is finally using File/Folder classes
|
||||
* @see https://github.com/nextcloud/server/blob/8a75cb6e72acd42712ab9fea22296aa1af863ef5/apps/files/src/views/favorites.ts#L83-L115
|
||||
*/
|
||||
const isDir = this.fileInfo.type === 'dir'
|
||||
const Node = isDir ? Folder : File
|
||||
emit(state ? 'files:favorites:added' : 'files:favorites:removed', new Node({
|
||||
fileid: this.fileInfo.id,
|
||||
source: this.davPath,
|
||||
root: `/files/${getCurrentUser().uid}`,
|
||||
mime: isDir ? undefined : this.fileInfo.mimetype,
|
||||
}))
|
||||
} catch (error) {
|
||||
OC.Notification.showTemporary(t('files', 'Unable to change the favourite state of the file'))
|
||||
console.error('Unable to change favourite state', error)
|
||||
|
|
@ -437,39 +451,41 @@ export default {
|
|||
* @throws {Error} loading failure
|
||||
*/
|
||||
async open(path) {
|
||||
if (!path || path.trim() === '') {
|
||||
throw new Error(`Invalid path '${path}'`)
|
||||
}
|
||||
|
||||
// update current opened file
|
||||
this.Sidebar.file = path
|
||||
|
||||
if (path && path.trim() !== '') {
|
||||
// reset data, keep old fileInfo to not reload all tabs and just hide them
|
||||
this.error = null
|
||||
this.loading = true
|
||||
// reset data, keep old fileInfo to not reload all tabs and just hide them
|
||||
this.error = null
|
||||
this.loading = true
|
||||
|
||||
try {
|
||||
this.fileInfo = await FileInfo(this.davPath)
|
||||
// adding this as fallback because other apps expect it
|
||||
this.fileInfo.dir = this.file.split('/').slice(0, -1).join('/')
|
||||
try {
|
||||
this.fileInfo = await FileInfo(this.davPath)
|
||||
// adding this as fallback because other apps expect it
|
||||
this.fileInfo.dir = this.file.split('/').slice(0, -1).join('/')
|
||||
|
||||
// DEPRECATED legacy views
|
||||
// TODO: remove
|
||||
this.views.forEach(view => {
|
||||
view.setFileInfo(this.fileInfo)
|
||||
})
|
||||
// DEPRECATED legacy views
|
||||
// TODO: remove
|
||||
this.views.forEach(view => {
|
||||
view.setFileInfo(this.fileInfo)
|
||||
})
|
||||
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.tabs) {
|
||||
this.$refs.tabs.updateTabs()
|
||||
}
|
||||
this.setActiveTab(this.Sidebar.activeTab || this.tabs[0].id)
|
||||
})
|
||||
} catch (error) {
|
||||
this.error = t('files', 'Error while loading the file data')
|
||||
console.error('Error while loading the file data', error)
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.tabs) {
|
||||
this.$refs.tabs.updateTabs()
|
||||
}
|
||||
this.setActiveTab(this.Sidebar.activeTab || this.tabs[0].id)
|
||||
})
|
||||
} catch (error) {
|
||||
this.error = t('files', 'Error while loading the file data')
|
||||
console.error('Error while loading the file data', error)
|
||||
|
||||
throw new Error(error)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
throw new Error(error)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import * as eventBus from '@nextcloud/event-bus'
|
|||
|
||||
import { action } from '../actions/favoriteAction'
|
||||
import * as favoritesService from '../services/Favorites'
|
||||
import NavigationService from '../services/Navigation'
|
||||
import { NavigationService } from '../services/Navigation'
|
||||
import registerFavoritesView from './favorites'
|
||||
|
||||
jest.mock('webdav/dist/node/request.js', () => ({
|
||||
|
|
|
|||
|
|
@ -19,8 +19,7 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
import type { Navigation } from '../services/Navigation'
|
||||
import type NavigationService from '../services/Navigation'
|
||||
import type { Navigation, NavigationService } from '../services/Navigation'
|
||||
import { getLanguage, translate as t } from '@nextcloud/l10n'
|
||||
import FolderSvg from '@mdi/svg/svg/folder.svg?raw'
|
||||
import StarSvg from '@mdi/svg/svg/star.svg?raw'
|
||||
|
|
|
|||
41
apps/files/src/views/files.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
/**
|
||||
* @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, Navigation } from '../services/Navigation'
|
||||
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import FolderSvg from '@mdi/svg/svg/folder.svg?raw'
|
||||
|
||||
import { getContents } from '../services/Files'
|
||||
|
||||
export default () => {
|
||||
const Navigation = window.OCP.Files.Navigation as NavigationService
|
||||
Navigation.register({
|
||||
id: 'files',
|
||||
name: t('files', 'All files'),
|
||||
caption: t('files', 'List of your files and folders.'),
|
||||
|
||||
icon: FolderSvg,
|
||||
order: 0,
|
||||
|
||||
getContents,
|
||||
} as Navigation)
|
||||
}
|
||||
|
|
@ -19,8 +19,7 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
import type NavigationService from '../services/Navigation'
|
||||
import type { Navigation } from '../services/Navigation'
|
||||
import type { NavigationService, Navigation } from '../services/Navigation'
|
||||
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import HistorySvg from '@mdi/svg/svg/history.svg?raw'
|
||||
|
|
|
|||
|
|
@ -1,41 +1,9 @@
|
|||
<?php /** @var \OCP\IL10N $l */ ?>
|
||||
<?php $_['appNavigation']->printPage(); ?>
|
||||
<!-- File navigation -->
|
||||
<div id="app-navigation-files" role="navigation"></div>
|
||||
|
||||
<!-- New files vue container -->
|
||||
<!-- File list vue container -->
|
||||
<div id="app-content-vue" class="hidden"></div>
|
||||
|
||||
<div id="app-content" tabindex="0">
|
||||
|
||||
<input type="checkbox" class="hidden-visually" id="showgridview"
|
||||
aria-label="<?php p($l->t('Toggle grid view'))?>"
|
||||
<?php if ($_['showgridview']) { ?>checked="checked" <?php } ?>/>
|
||||
<label id="view-toggle" for="showgridview" tabindex="0" class="button <?php p($_['showgridview'] ? 'icon-toggle-filelist' : 'icon-toggle-pictures') ?>"
|
||||
title="<?php p($_['showgridview'] ? $l->t('Show list view') : $l->t('Show grid view'))?>"></label>
|
||||
|
||||
|
||||
<!-- Legacy views -->
|
||||
<?php foreach ($_['appContents'] as $content) { ?>
|
||||
<div id="app-content-<?php p($content['id']) ?>" class="hidden viewcontainer">
|
||||
<?php print_unescaped($content['content']) ?>
|
||||
</div>
|
||||
<?php } ?>
|
||||
<div id="searchresults" class="hidden"></div>
|
||||
</div><!-- closing app-content -->
|
||||
|
||||
<!-- config hints for javascript -->
|
||||
<input type="hidden" name="filesApp" id="filesApp" value="1" />
|
||||
<input type="hidden" name="usedSpacePercent" id="usedSpacePercent" value="<?php p($_['usedSpacePercent']); ?>" />
|
||||
<input type="hidden" name="owner" id="owner" value="<?php p($_['owner']); ?>" />
|
||||
<input type="hidden" name="ownerDisplayName" id="ownerDisplayName" value="<?php p($_['ownerDisplayName']); ?>" />
|
||||
<input type="hidden" name="fileNotFound" id="fileNotFound" value="<?php p($_['fileNotFound']); ?>" />
|
||||
<?php if (!$_['isPublic']) :?>
|
||||
<input type="hidden" name="allowShareWithLink" id="allowShareWithLink" value="<?php p($_['allowShareWithLink']) ?>" />
|
||||
<input type="hidden" name="defaultFileSorting" id="defaultFileSorting" value="<?php p($_['defaultFileSorting']) ?>" />
|
||||
<input type="hidden" name="defaultFileSortingDirection" id="defaultFileSortingDirection" value="<?php p($_['defaultFileSortingDirection']) ?>" />
|
||||
<input type="hidden" name="showHiddenFiles" id="showHiddenFiles" value="<?php p($_['showHiddenFiles']); ?>" />
|
||||
<input type="hidden" name="cropImagePreviews" id="cropImagePreviews" value="<?php p($_['cropImagePreviews']); ?>" />
|
||||
<?php endif;
|
||||
|
||||
foreach ($_['hiddenFields'] as $name => $value) {?>
|
||||
<input type="hidden" name="<?php p($name) ?>" id="<?php p($name) ?>" value="<?php p($value) ?>" />
|
||||
<?php }
|
||||
|
|
|
|||
|
|
@ -163,74 +163,28 @@ class ViewControllerTest extends TestCase {
|
|||
[$this->user->getUID(), 'files', 'crop_image_previews', true, true],
|
||||
[$this->user->getUID(), 'files', 'show_grid', true],
|
||||
]);
|
||||
|
||||
$baseFolderFiles = $this->getMockBuilder(Folder::class)->getMock();
|
||||
|
||||
$this->rootFolder->expects($this->any())
|
||||
->method('getUserFolder')
|
||||
->with('testuser1')
|
||||
->willReturn($baseFolderFiles);
|
||||
|
||||
$this->config
|
||||
->expects($this->any())
|
||||
->method('getAppValue')
|
||||
->willReturnArgument(2);
|
||||
$this->shareManager->method('shareApiAllowLinks')
|
||||
->willReturn(true);
|
||||
|
||||
$nav = new Template('files', 'appnavigation');
|
||||
$nav->assign('navigationItems', [
|
||||
'files' => [
|
||||
'id' => 'files',
|
||||
'appname' => 'files',
|
||||
'script' => 'list.php',
|
||||
'order' => 0,
|
||||
'name' => \OC::$server->getL10N('files')->t('All files'),
|
||||
'active' => false,
|
||||
'icon' => '',
|
||||
'type' => 'link',
|
||||
'classes' => '',
|
||||
'expanded' => false,
|
||||
'unread' => 0,
|
||||
],
|
||||
'systemtagsfilter' => [
|
||||
'id' => 'systemtagsfilter',
|
||||
'appname' => 'systemtags',
|
||||
'script' => 'list.php',
|
||||
'order' => 25,
|
||||
'name' => \OC::$server->getL10N('systemtags')->t('Tags'),
|
||||
'active' => false,
|
||||
'icon' => '',
|
||||
'type' => 'link',
|
||||
'classes' => '',
|
||||
'expanded' => false,
|
||||
'unread' => 0,
|
||||
],
|
||||
]);
|
||||
|
||||
$expected = new Http\TemplateResponse(
|
||||
'files',
|
||||
'index',
|
||||
[
|
||||
'usedSpacePercent' => 123,
|
||||
'owner' => 'MyName',
|
||||
'ownerDisplayName' => 'MyDisplayName',
|
||||
'isPublic' => false,
|
||||
'defaultFileSorting' => 'basename',
|
||||
'defaultFileSortingDirection' => 'asc',
|
||||
'showHiddenFiles' => 0,
|
||||
'cropImagePreviews' => 1,
|
||||
'fileNotFound' => 0,
|
||||
'allowShareWithLink' => 'yes',
|
||||
'appNavigation' => $nav,
|
||||
'appContents' => [
|
||||
'files' => [
|
||||
'id' => 'files',
|
||||
'content' => null,
|
||||
],
|
||||
'systemtagsfilter' => [
|
||||
'id' => 'systemtagsfilter',
|
||||
'content' => null,
|
||||
],
|
||||
],
|
||||
'hiddenFields' => [],
|
||||
'showgridview' => null
|
||||
]
|
||||
);
|
||||
$policy = new Http\ContentSecurityPolicy();
|
||||
$policy->addAllowedWorkerSrcDomain('\'self\'');
|
||||
$policy->addAllowedFrameDomain('\'self\'');
|
||||
$expected->setContentSecurityPolicy($policy);
|
||||
|
||||
|
|
@ -249,100 +203,6 @@ class ViewControllerTest extends TestCase {
|
|||
$this->assertEquals($expected, $this->viewController->index('MyDir', 'MyView'));
|
||||
}
|
||||
|
||||
public function testShowFileRouteWithFolder() {
|
||||
$node = $this->getMockBuilder(Folder::class)->getMock();
|
||||
$node->expects($this->once())
|
||||
->method('getPath')
|
||||
->willReturn('/testuser1/files/test/sub');
|
||||
|
||||
$baseFolder = $this->getMockBuilder(Folder::class)->getMock();
|
||||
|
||||
$this->rootFolder->expects($this->once())
|
||||
->method('getUserFolder')
|
||||
->with('testuser1')
|
||||
->willReturn($baseFolder);
|
||||
|
||||
$baseFolder->expects($this->once())
|
||||
->method('getById')
|
||||
->with(123)
|
||||
->willReturn([$node]);
|
||||
$baseFolder->expects($this->once())
|
||||
->method('getRelativePath')
|
||||
->with('/testuser1/files/test/sub')
|
||||
->willReturn('/test/sub');
|
||||
|
||||
$this->urlGenerator
|
||||
->expects($this->once())
|
||||
->method('linkToRoute')
|
||||
->with('files.view.index', ['dir' => '/test/sub'])
|
||||
->willReturn('/apps/files/?dir=/test/sub');
|
||||
|
||||
$expected = new Http\RedirectResponse('/apps/files/?dir=/test/sub');
|
||||
$this->assertEquals($expected, $this->viewController->index('', '', '123'));
|
||||
}
|
||||
|
||||
public function testShowFileRouteWithFile() {
|
||||
$parentNode = $this->getMockBuilder(Folder::class)->getMock();
|
||||
$parentNode->expects($this->once())
|
||||
->method('getPath')
|
||||
->willReturn('testuser1/files/test');
|
||||
|
||||
$baseFolder = $this->getMockBuilder(Folder::class)->getMock();
|
||||
|
||||
$this->rootFolder->expects($this->once())
|
||||
->method('getUserFolder')
|
||||
->with('testuser1')
|
||||
->willReturn($baseFolder);
|
||||
|
||||
$node = $this->getMockBuilder(File::class)->getMock();
|
||||
$node->expects($this->once())
|
||||
->method('getParent')
|
||||
->willReturn($parentNode);
|
||||
$node->expects($this->once())
|
||||
->method('getName')
|
||||
->willReturn('somefile.txt');
|
||||
|
||||
$baseFolder->expects($this->once())
|
||||
->method('getById')
|
||||
->with(123)
|
||||
->willReturn([$node]);
|
||||
$baseFolder->expects($this->once())
|
||||
->method('getRelativePath')
|
||||
->with('testuser1/files/test')
|
||||
->willReturn('/test');
|
||||
|
||||
$this->urlGenerator
|
||||
->expects($this->once())
|
||||
->method('linkToRoute')
|
||||
->with('files.view.index', ['dir' => '/test', 'scrollto' => 'somefile.txt'])
|
||||
->willReturn('/apps/files/?dir=/test/sub&scrollto=somefile.txt');
|
||||
|
||||
$expected = new Http\RedirectResponse('/apps/files/?dir=/test/sub&scrollto=somefile.txt');
|
||||
$this->assertEquals($expected, $this->viewController->index('', '', '123'));
|
||||
}
|
||||
|
||||
public function testShowFileRouteWithInvalidFileId() {
|
||||
$baseFolder = $this->getMockBuilder(Folder::class)->getMock();
|
||||
$this->rootFolder->expects($this->once())
|
||||
->method('getUserFolder')
|
||||
->with('testuser1')
|
||||
->willReturn($baseFolder);
|
||||
|
||||
$baseFolder->expects($this->once())
|
||||
->method('getById')
|
||||
->with(123)
|
||||
->willReturn([]);
|
||||
|
||||
$this->urlGenerator->expects($this->once())
|
||||
->method('linkToRoute')
|
||||
->with('files.view.index', ['fileNotFound' => true])
|
||||
->willReturn('redirect.url');
|
||||
|
||||
$response = $this->viewController->index('', 'MyView', '123');
|
||||
$this->assertInstanceOf('OCP\AppFramework\Http\RedirectResponse', $response);
|
||||
$this->assertEquals('redirect.url', $response->getRedirectURL());
|
||||
}
|
||||
|
||||
public function testShowFileRouteWithTrashedFile() {
|
||||
$this->appManager->expects($this->once())
|
||||
->method('isEnabledForUser')
|
||||
|
|
@ -357,7 +217,7 @@ class ViewControllerTest extends TestCase {
|
|||
$baseFolderFiles = $this->getMockBuilder(Folder::class)->getMock();
|
||||
$baseFolderTrash = $this->getMockBuilder(Folder::class)->getMock();
|
||||
|
||||
$this->rootFolder->expects($this->once())
|
||||
$this->rootFolder->expects($this->any())
|
||||
->method('getUserFolder')
|
||||
->with('testuser1')
|
||||
->willReturn($baseFolderFiles);
|
||||
|
|
@ -366,7 +226,7 @@ class ViewControllerTest extends TestCase {
|
|||
->with('testuser1/files_trashbin/files/')
|
||||
->willReturn($baseFolderTrash);
|
||||
|
||||
$baseFolderFiles->expects($this->once())
|
||||
$baseFolderFiles->expects($this->any())
|
||||
->method('getById')
|
||||
->with(123)
|
||||
->willReturn([]);
|
||||
|
|
@ -375,9 +235,6 @@ class ViewControllerTest extends TestCase {
|
|||
$node->expects($this->once())
|
||||
->method('getParent')
|
||||
->willReturn($parentNode);
|
||||
$node->expects($this->once())
|
||||
->method('getName')
|
||||
->willReturn('somefile.txt');
|
||||
|
||||
$baseFolderTrash->expects($this->once())
|
||||
->method('getById')
|
||||
|
|
@ -391,10 +248,10 @@ class ViewControllerTest extends TestCase {
|
|||
$this->urlGenerator
|
||||
->expects($this->once())
|
||||
->method('linkToRoute')
|
||||
->with('files.view.index', ['view' => 'trashbin', 'dir' => '/test.d1462861890/sub', 'scrollto' => 'somefile.txt'])
|
||||
->willReturn('/apps/files/?view=trashbin&dir=/test.d1462861890/sub&scrollto=somefile.txt');
|
||||
->with('files.view.indexViewFileid', ['view' => 'trashbin', 'dir' => '/test.d1462861890/sub', 'fileid' => '123'])
|
||||
->willReturn('/apps/files/trashbin/123?dir=/test.d1462861890/sub');
|
||||
|
||||
$expected = new Http\RedirectResponse('/apps/files/?view=trashbin&dir=/test.d1462861890/sub&scrollto=somefile.txt');
|
||||
$expected = new Http\RedirectResponse('/apps/files/trashbin/123?dir=/test.d1462861890/sub');
|
||||
$this->assertEquals($expected, $this->viewController->index('', '', '123'));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,8 +19,7 @@
|
|||
* 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 type { NavigationService, Navigation } from '../../files/src/services/Navigation'
|
||||
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
|
|
|
|||
|
|
@ -19,14 +19,14 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
import { action } from './openInFilesAction'
|
||||
import type { Navigation } from '../../../files/src/services/Navigation'
|
||||
|
||||
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 { action } from './openInFilesAction'
|
||||
import { DefaultType, FileAction } from '../../../files/src/services/FileAction'
|
||||
import { deletedSharesViewId, pendingSharesViewId, sharedWithOthersViewId, sharedWithYouViewId, sharesViewId, sharingByLinksViewId } from '../views/shares'
|
||||
|
||||
const view = {
|
||||
|
|
@ -92,6 +92,6 @@ describe('Open in files action execute tests', () => {
|
|||
// Silent action
|
||||
expect(exec).toBe(null)
|
||||
expect(goToRouteMock).toBeCalledTimes(1)
|
||||
expect(goToRouteMock).toBeCalledWith(null, { fileid: 1, view: 'files' }, { fileid: 1, dir: '/Foo' })
|
||||
expect(goToRouteMock).toBeCalledWith(null, { fileid: 1, view: 'files' }, { dir: '/Foo' })
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export const action = new FileAction({
|
|||
window.OCP.Files.Router.goToRoute(
|
||||
null, // use default route
|
||||
{ view: 'files', fileid: node.fileid },
|
||||
{ dir: node.dirname, fileid: node.fileid },
|
||||
{ dir: node.dirname },
|
||||
)
|
||||
return null
|
||||
},
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ 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 { NavigationService } from '../../../files/src/services/Navigation'
|
||||
import registerSharingViews from './shares'
|
||||
|
||||
import '../main'
|
||||
|
|
|
|||
|
|
@ -19,8 +19,7 @@
|
|||
* 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 type { NavigationService, Navigation } from '../../../files/src/services/Navigation'
|
||||
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import AccountClockSvg from '@mdi/svg/svg/account-clock.svg?raw'
|
||||
|
|
|
|||
|
|
@ -19,8 +19,7 @@
|
|||
* 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 type { NavigationService, Navigation } from '../../files/src/services/Navigation'
|
||||
|
||||
import { translate as t, translate } from '@nextcloud/l10n'
|
||||
import DeleteSvg from '@mdi/svg/svg/delete.svg?raw'
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
*/
|
||||
/* eslint-disable */
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
import { File, Folder, parseWebdavPermissions } from '@nextcloud/files'
|
||||
import { File, Folder, davParsePermissions } from '@nextcloud/files'
|
||||
import { generateRemoteUrl, generateUrl } from '@nextcloud/router'
|
||||
|
||||
import type { FileStat, ResponseDataDetailed } from 'webdav'
|
||||
|
|
@ -43,7 +43,7 @@ const data = `<?xml version="1.0"?>
|
|||
|
||||
|
||||
const resultToNode = function(node: FileStat): File | Folder {
|
||||
const permissions = parseWebdavPermissions(node.props?.permissions)
|
||||
const permissions = davParsePermissions(node.props?.permissions)
|
||||
const owner = getCurrentUser()?.uid as string
|
||||
const previewUrl = generateUrl('/apps/files_trashbin/preview?fileId={fileid}&x=32&y=32', node.props)
|
||||
|
||||
|
|
|
|||
|
|
@ -60,6 +60,9 @@ window.addEventListener('DOMContentLoaded', function() {
|
|||
TabInstance.update(fileInfo)
|
||||
},
|
||||
setIsActive(isActive) {
|
||||
if (!TabInstance) {
|
||||
return
|
||||
}
|
||||
TabInstance.setIsActive(isActive)
|
||||
},
|
||||
destroy() {
|
||||
|
|
|
|||
|
|
@ -202,8 +202,8 @@ class UtilTest extends TestCase {
|
|||
public function dataGetAppImage() {
|
||||
return [
|
||||
['core', 'logo/logo.svg', \OC::$SERVERROOT . '/core/img/logo/logo.svg'],
|
||||
['files', 'external', \OC::$SERVERROOT . '/apps/files/img/external.svg'],
|
||||
['files', 'external.svg', \OC::$SERVERROOT . '/apps/files/img/external.svg'],
|
||||
['files', 'folder', \OC::$SERVERROOT . '/apps/files/img/folder.svg'],
|
||||
['files', 'folder.svg', \OC::$SERVERROOT . '/apps/files/img/folder.svg'],
|
||||
['noapplikethis', 'foobar.svg', false],
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,12 +32,14 @@ use OCP\AppFramework\Controller;
|
|||
use OCP\AppFramework\Http;
|
||||
use OCP\AppFramework\Http\DataResponse;
|
||||
use OCP\AppFramework\Http\FileDisplayResponse;
|
||||
use OCP\AppFramework\Http\RedirectResponse;
|
||||
use OCP\Files\File;
|
||||
use OCP\Files\IRootFolder;
|
||||
use OCP\Files\Node;
|
||||
use OCP\Files\NotFoundException;
|
||||
use OCP\IPreview;
|
||||
use OCP\IRequest;
|
||||
use OCP\Preview\IMimeIconProvider;
|
||||
|
||||
class PreviewController extends Controller {
|
||||
public function __construct(
|
||||
|
|
@ -46,6 +48,7 @@ class PreviewController extends Controller {
|
|||
private IPreview $preview,
|
||||
private IRootFolder $root,
|
||||
private ?string $userId,
|
||||
private IMimeIconProvider $mimeIconProvider,
|
||||
) {
|
||||
parent::__construct($appName, $request);
|
||||
}
|
||||
|
|
@ -62,9 +65,11 @@ class PreviewController extends Controller {
|
|||
* @param bool $a Whether to not crop the preview
|
||||
* @param bool $forceIcon Force returning an icon
|
||||
* @param string $mode How to crop the image
|
||||
* @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, array<empty>, array{}>
|
||||
* @param bool $mimeFallback Whether to fallback to the mime icon if no preview is available
|
||||
* @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, array<empty>, array{}>|RedirectResponse<Http::STATUS_SEE_OTHER, array{}>
|
||||
*
|
||||
* 200: Preview returned
|
||||
* 303: Redirect to the mime icon url if mimeFallback is true
|
||||
* 400: Getting preview is not possible
|
||||
* 403: Getting preview is not allowed
|
||||
* 404: Preview not found
|
||||
|
|
@ -75,7 +80,8 @@ class PreviewController extends Controller {
|
|||
int $y = 32,
|
||||
bool $a = false,
|
||||
bool $forceIcon = true,
|
||||
string $mode = 'fill'): Http\Response {
|
||||
string $mode = 'fill',
|
||||
bool $mimeFallback = false): Http\Response {
|
||||
if ($file === '' || $x === 0 || $y === 0) {
|
||||
return new DataResponse([], Http::STATUS_BAD_REQUEST);
|
||||
}
|
||||
|
|
@ -87,7 +93,7 @@ class PreviewController extends Controller {
|
|||
return new DataResponse([], Http::STATUS_NOT_FOUND);
|
||||
}
|
||||
|
||||
return $this->fetchPreview($node, $x, $y, $a, $forceIcon, $mode);
|
||||
return $this->fetchPreview($node, $x, $y, $a, $forceIcon, $mode, $mimeFallback);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -102,9 +108,11 @@ class PreviewController extends Controller {
|
|||
* @param bool $a Whether to not crop the preview
|
||||
* @param bool $forceIcon Force returning an icon
|
||||
* @param string $mode How to crop the image
|
||||
* @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, array<empty>, array{}>
|
||||
* @param bool $mimeFallback Whether to fallback to the mime icon if no preview is available
|
||||
* @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, array<empty>, array{}>|RedirectResponse<Http::STATUS_SEE_OTHER, array{}>
|
||||
*
|
||||
* 200: Preview returned
|
||||
* 303: Redirect to the mime icon url if mimeFallback is true
|
||||
* 400: Getting preview is not possible
|
||||
* 403: Getting preview is not allowed
|
||||
* 404: Preview not found
|
||||
|
|
@ -115,7 +123,8 @@ class PreviewController extends Controller {
|
|||
int $y = 32,
|
||||
bool $a = false,
|
||||
bool $forceIcon = true,
|
||||
string $mode = 'fill') {
|
||||
string $mode = 'fill',
|
||||
bool $mimeFallback = false) {
|
||||
if ($fileId === -1 || $x === 0 || $y === 0) {
|
||||
return new DataResponse([], Http::STATUS_BAD_REQUEST);
|
||||
}
|
||||
|
|
@ -129,11 +138,11 @@ class PreviewController extends Controller {
|
|||
|
||||
$node = array_pop($nodes);
|
||||
|
||||
return $this->fetchPreview($node, $x, $y, $a, $forceIcon, $mode);
|
||||
return $this->fetchPreview($node, $x, $y, $a, $forceIcon, $mode, $mimeFallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, array<empty>, array{}>
|
||||
* @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, array<empty>, array{}>|RedirectResponse<Http::STATUS_SEE_OTHER, array{}>
|
||||
*/
|
||||
private function fetchPreview(
|
||||
Node $node,
|
||||
|
|
@ -141,7 +150,8 @@ class PreviewController extends Controller {
|
|||
int $y,
|
||||
bool $a,
|
||||
bool $forceIcon,
|
||||
string $mode) : Http\Response {
|
||||
string $mode,
|
||||
bool $mimeFallback = false) : Http\Response {
|
||||
if (!($node instanceof File) || (!$forceIcon && !$this->preview->isAvailable($node))) {
|
||||
return new DataResponse([], Http::STATUS_NOT_FOUND);
|
||||
}
|
||||
|
|
@ -167,6 +177,13 @@ class PreviewController extends Controller {
|
|||
$response->cacheFor(3600 * 24, false, true);
|
||||
return $response;
|
||||
} catch (NotFoundException $e) {
|
||||
// If we have no preview enabled, we can redirect to the mime icon if any
|
||||
if ($mimeFallback) {
|
||||
if ($url = $this->mimeIconProvider->getMimeIconUrl($node->getMimeType())) {
|
||||
return new RedirectResponse($url);
|
||||
}
|
||||
}
|
||||
|
||||
return new DataResponse([], Http::STATUS_NOT_FOUND);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return new DataResponse([], Http::STATUS_BAD_REQUEST);
|
||||
|
|
|
|||
|
|
@ -939,6 +939,15 @@
|
|||
"type": "string",
|
||||
"default": "fill"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "mimeFallback",
|
||||
"in": "query",
|
||||
"description": "Whether to fallback to the mime icon if no preview is available",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"default": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
|
|
@ -976,6 +985,16 @@
|
|||
"schema": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"303": {
|
||||
"description": "Redirect to the mime icon url if mimeFallback is true",
|
||||
"headers": {
|
||||
"Location": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1051,6 +1070,15 @@
|
|||
"type": "string",
|
||||
"default": "fill"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "mimeFallback",
|
||||
"in": "query",
|
||||
"description": "Whether to fallback to the mime icon if no preview is available",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"default": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
|
|
@ -1088,6 +1116,16 @@
|
|||
"schema": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"303": {
|
||||
"description": "Redirect to the mime icon url if mimeFallback is true",
|
||||
"headers": {
|
||||
"Location": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,52 +24,53 @@
|
|||
*/
|
||||
|
||||
(function(OC) {
|
||||
|
||||
_.extend(OC.Files.Client, {
|
||||
PROPERTY_FILEID: '{' + OC.Files.Client.NS_OWNCLOUD + '}id',
|
||||
PROPERTY_CAN_ASSIGN: '{' + OC.Files.Client.NS_OWNCLOUD + '}can-assign',
|
||||
PROPERTY_DISPLAYNAME: '{' + OC.Files.Client.NS_OWNCLOUD + '}display-name',
|
||||
PROPERTY_USERVISIBLE: '{' + OC.Files.Client.NS_OWNCLOUD + '}user-visible',
|
||||
PROPERTY_USERASSIGNABLE: '{' + OC.Files.Client.NS_OWNCLOUD + '}user-assignable',
|
||||
})
|
||||
|
||||
/**
|
||||
* @class OCA.SystemTags.SystemTagsCollection
|
||||
* @classdesc
|
||||
*
|
||||
* System tag
|
||||
*
|
||||
*/
|
||||
const SystemTagModel = OC.Backbone.Model.extend(
|
||||
/** @lends OCA.SystemTags.SystemTagModel.prototype */ {
|
||||
sync: OC.Backbone.davSync,
|
||||
|
||||
defaults: {
|
||||
userVisible: true,
|
||||
userAssignable: true,
|
||||
canAssign: true,
|
||||
},
|
||||
|
||||
davProperties: {
|
||||
id: OC.Files.Client.PROPERTY_FILEID,
|
||||
name: OC.Files.Client.PROPERTY_DISPLAYNAME,
|
||||
userVisible: OC.Files.Client.PROPERTY_USERVISIBLE,
|
||||
userAssignable: OC.Files.Client.PROPERTY_USERASSIGNABLE,
|
||||
// read-only, effective permissions computed by the server,
|
||||
canAssign: OC.Files.Client.PROPERTY_CAN_ASSIGN,
|
||||
},
|
||||
|
||||
parse(data) {
|
||||
return {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
userVisible: data.userVisible === true || data.userVisible === 'true',
|
||||
userAssignable: data.userAssignable === true || data.userAssignable === 'true',
|
||||
canAssign: data.canAssign === true || data.canAssign === 'true',
|
||||
}
|
||||
},
|
||||
if (OC?.Files?.Client) {
|
||||
_.extend(OC.Files.Client, {
|
||||
PROPERTY_FILEID: '{' + OC.Files.Client.NS_OWNCLOUD + '}id',
|
||||
PROPERTY_CAN_ASSIGN: '{' + OC.Files.Client.NS_OWNCLOUD + '}can-assign',
|
||||
PROPERTY_DISPLAYNAME: '{' + OC.Files.Client.NS_OWNCLOUD + '}display-name',
|
||||
PROPERTY_USERVISIBLE: '{' + OC.Files.Client.NS_OWNCLOUD + '}user-visible',
|
||||
PROPERTY_USERASSIGNABLE: '{' + OC.Files.Client.NS_OWNCLOUD + '}user-assignable',
|
||||
})
|
||||
|
||||
OC.SystemTags = OC.SystemTags || {}
|
||||
OC.SystemTags.SystemTagModel = SystemTagModel
|
||||
/**
|
||||
* @class OCA.SystemTags.SystemTagsCollection
|
||||
* @classdesc
|
||||
*
|
||||
* System tag
|
||||
*
|
||||
*/
|
||||
const SystemTagModel = OC.Backbone.Model.extend(
|
||||
/** @lends OCA.SystemTags.SystemTagModel.prototype */ {
|
||||
sync: OC.Backbone.davSync,
|
||||
|
||||
defaults: {
|
||||
userVisible: true,
|
||||
userAssignable: true,
|
||||
canAssign: true,
|
||||
},
|
||||
|
||||
davProperties: {
|
||||
id: OC.Files.Client.PROPERTY_FILEID,
|
||||
name: OC.Files.Client.PROPERTY_DISPLAYNAME,
|
||||
userVisible: OC.Files.Client.PROPERTY_USERVISIBLE,
|
||||
userAssignable: OC.Files.Client.PROPERTY_USERASSIGNABLE,
|
||||
// read-only, effective permissions computed by the server,
|
||||
canAssign: OC.Files.Client.PROPERTY_CAN_ASSIGN,
|
||||
},
|
||||
|
||||
parse(data) {
|
||||
return {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
userVisible: data.userVisible === true || data.userVisible === 'true',
|
||||
userAssignable: data.userAssignable === true || data.userAssignable === 'true',
|
||||
canAssign: data.canAssign === true || data.canAssign === 'true',
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
OC.SystemTags = OC.SystemTags || {}
|
||||
OC.SystemTags.SystemTagModel = SystemTagModel
|
||||
}
|
||||
})(OC)
|
||||
|
|
|
|||
|
|
@ -32,6 +32,6 @@ describe('Login with a new user and open the files app', function() {
|
|||
|
||||
it('See the default file welcome.txt in the files list', function() {
|
||||
cy.visit('/apps/files')
|
||||
cy.get('.files-fileList tr').should('contain', 'welcome.txt')
|
||||
cy.get('[data-cy-files-list] [data-cy-files-list-row-name="welcome.txt"]').should('be.visible')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -36,15 +36,15 @@ export function uploadThreeVersions(user: User, fileName: string) {
|
|||
}
|
||||
|
||||
export function openVersionsPanel(fileName: string) {
|
||||
cy.get(`[data-file="${fileName}"]`).within(() => {
|
||||
cy.get('[data-action="menu"]')
|
||||
.click()
|
||||
|
||||
cy.get('.fileActionsMenu')
|
||||
.get('.action-details')
|
||||
cy.get(`[data-cy-files-list] [data-cy-files-list-row-name="${fileName}"]`).within(() => {
|
||||
cy.get('[data-cy-files-list-row-actions] .action-item__menutoggle')
|
||||
.click()
|
||||
})
|
||||
|
||||
cy.get('.action-item__popper')
|
||||
.get('[data-cy-files-list-row-action="details"]')
|
||||
.click()
|
||||
|
||||
cy.get('#app-sidebar-vue')
|
||||
.get('[aria-controls="tab-version_vue"]')
|
||||
.click()
|
||||
|
|
|
|||
4
dist/1929-1929.js
vendored
2
dist/1929-1929.js.map
vendored
4
dist/core-common.js
vendored
4
dist/core-common.js.LICENSE.txt
vendored
|
|
@ -124,8 +124,12 @@
|
|||
* Copyright (c) 2016 Jorik Tangelder;
|
||||
* Licensed under the MIT license */
|
||||
|
||||
/*! https://mths.be/punycode v1.4.1 by @mathias */
|
||||
|
||||
/*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh <https://feross.org/opensource> */
|
||||
|
||||
/*! safe-buffer. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
|
||||
|
||||
/**
|
||||
* @copyright Copyright (c) 2019 Georg Ehrke
|
||||
*
|
||||
|
|
|
|||
2
dist/core-common.js.map
vendored
4
dist/core-login.js
vendored
44
dist/core-login.js.LICENSE.txt
vendored
|
|
@ -1,27 +1,5 @@
|
|||
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
|
||||
|
||||
/**
|
||||
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @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 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
|
|
@ -188,25 +166,3 @@
|
|||
* 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/>.
|
||||
*
|
||||
*/
|
||||
|
|
|
|||
2
dist/core-login.js.map
vendored
4
dist/core-main.js
vendored
44
dist/core-main.js.LICENSE.txt
vendored
|
|
@ -327,28 +327,6 @@
|
|||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @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 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
|
|
@ -587,28 +565,6 @@
|
|||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* @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 Joas Schilling <coding@schilljs.com>
|
||||
*
|
||||
|
|
|
|||
2
dist/core-main.js.map
vendored
4
dist/core-systemtags.js
vendored
2
dist/core-systemtags.js.map
vendored
4
dist/files-main.js
vendored
49
dist/files-main.js.LICENSE.txt
vendored
|
|
@ -38,57 +38,8 @@
|
|||
|
||||
/*! For license information please see NcInputField.js.LICENSE.txt */
|
||||
|
||||
/*! https://mths.be/punycode v1.4.1 by @mathias */
|
||||
|
||||
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
|
||||
|
||||
/*! safe-buffer. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
|
||||
|
||||
/**
|
||||
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @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 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
* @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) 2019 Gary Kim <gary@garykim.dev>
|
||||
*
|
||||
|
|
|
|||
2
dist/files-main.js.map
vendored
4
dist/files-sidebar.js
vendored
2
dist/files-sidebar.js.map
vendored
4
dist/files_external-main.js
vendored
44
dist/files_external-main.js.LICENSE.txt
vendored
|
|
@ -1,49 +1,5 @@
|
|||
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
|
||||
|
||||
/**
|
||||
* @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>
|
||||
*
|
||||
|
|
|
|||
2
dist/files_external-main.js.map
vendored
4
dist/files_sharing-files_sharing.js
vendored
44
dist/files_sharing-files_sharing.js.LICENSE.txt
vendored
|
|
@ -23,50 +23,6 @@
|
|||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* @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>
|
||||
*
|
||||
|
|
|
|||