Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
29a7f7f
feat(files_trashbin): migrate to vue
skjnldsv Jan 13, 2023
03c3277
feat(files): switch to pinia
skjnldsv Feb 4, 2023
638b3df
perf(files): update files store by chunks
skjnldsv Feb 4, 2023
2ff1c00
fix(files_trashbin): previews crop support
skjnldsv Feb 5, 2023
b761039
perf(files): fetch previews faster and cache properly
skjnldsv Mar 17, 2023
10010fc
feat(files): sorting
skjnldsv Mar 21, 2023
f330813
feat(files): custom columns
skjnldsv Mar 22, 2023
0db210a
chore(deps): cleanup unused deps and audit
skjnldsv Mar 22, 2023
0b4da61
feat(files): actions api
skjnldsv Mar 23, 2023
3c3050c
feat(files): implement sorting per view
skjnldsv Mar 24, 2023
0e764f7
fix(files): fix custom render components reactivity
skjnldsv Mar 24, 2023
0f717d4
feat(accessibility): add files table caption and summary
skjnldsv Mar 24, 2023
e85eb4c
fix(files): selection and render performance
skjnldsv Mar 24, 2023
f28944e
feat(files): propagate restore and delete events
skjnldsv Mar 25, 2023
bda286c
perf(files): less verbose
skjnldsv Mar 25, 2023
6358e97
fix(files): inline action styling
skjnldsv Mar 25, 2023
2b25199
fix(files): accessibility tab into recycled invisible files rows
skjnldsv Mar 25, 2023
7215a9a
fix(files): breadcrumbs accessibility title
skjnldsv Mar 28, 2023
4942747
fix(files): use inline NcActions
skjnldsv Mar 28, 2023
60b74e3
feat(files): batch actions
skjnldsv Mar 28, 2023
044e824
chore(deps): update lockfile
skjnldsv Mar 28, 2023
c7c9ee1
feat(files): move userconfig to dedicated store and fix crop previews
skjnldsv Mar 31, 2023
a66cae0
fix(deps): update webdav 5 usage
skjnldsv Mar 31, 2023
014a57e
fix: improved preview handling
skjnldsv Apr 4, 2023
bdbe477
feat(files): add FileAction service
skjnldsv Apr 4, 2023
904348b
chore(npm): build assets
skjnldsv Apr 4, 2023
1361182
chore(eslint): clean and fix
skjnldsv Apr 4, 2023
f060e5a
fix(tests): update jsunit tests after dep and files update
skjnldsv Apr 4, 2023
d432e0c
fix(cypress): component testing with pinia
skjnldsv Apr 5, 2023
8298bb4
fix:(files-checker): add cypress.d.ts and custom.d.ts
skjnldsv Apr 5, 2023
ea3e77d
fix(files): better wording and catch single action run
skjnldsv Apr 5, 2023
5b3900e
fix(tests): acceptance
skjnldsv Apr 6, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 10 additions & 11 deletions apps/comments/src/services/DavClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,18 @@
*
*/

import { createClient, getPatcher } from 'webdav'
import axios from '@nextcloud/axios'

import { createClient } from 'webdav'
import { getRootPath } from '../utils/davUtils.js'

// Add this so the server knows it is an request from the browser
axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'

// force our axios
const patcher = getPatcher()
patcher.patch('request', axios)
import { getRequestToken } from '@nextcloud/auth'

// init webdav client
const client = createClient(getRootPath())
const client = createClient(getRootPath(), {
headers: {
// Add this so the server knows it is an request from the browser
'X-Requested-With': 'XMLHttpRequest',
// Inject user auth
requesttoken: getRequestToken() ?? '',
},
})

export default client
Original file line number Diff line number Diff line change
Expand Up @@ -20,26 +20,29 @@
*
*/

import { parseXML, prepareFileFromProps } from 'webdav/dist/node/tools/dav.js'
import { processResponsePayload } from 'webdav/dist/node/response.js'
import { decodeHtmlEntities } from '../utils/decodeHtmlEntities.js'
import { parseXML, type DAVResult, type FileStat } from 'webdav'

// https://github.com/perry-mitchell/webdav-client/issues/339
import { processResponsePayload } from '../../../../node_modules/webdav/dist/node/response.js'
import { prepareFileFromProps } from '../../../../node_modules/webdav/dist/node/tools/dav.js'
import client from './DavClient.js'

export const DEFAULT_LIMIT = 20

/**
* Retrieve the comments list
*
* @param {object} data destructuring object
* @param {string} data.commentsType the ressource type
* @param {number} data.ressourceId the ressource ID
* @param {object} [options] optional options for axios
* @param {number} [options.offset] the pagination offset
* @return {object[]} the comments list
*/
export default async function({ commentsType, ressourceId }, options = {}) {
let response = null
export const getComments = async function({ commentsType, ressourceId }, options: { offset: number }) {
const ressourcePath = ['', commentsType, ressourceId].join('/')

return await client.customRequest(ressourcePath, Object.assign({
const response = await client.customRequest(ressourcePath, Object.assign({
method: 'REPORT',
data: `<?xml version="1.0"?>
<oc:filter-comments
Expand All @@ -51,42 +54,30 @@ export default async function({ commentsType, ressourceId }, options = {}) {
<oc:offset>${options.offset || 0}</oc:offset>
</oc:filter-comments>`,
}, options))
// See example on how it's done normally
// https://github.com/perry-mitchell/webdav-client/blob/9de2da4a2599e06bd86c2778145b7ade39fe0b3c/source/interface/stat.js#L19
// Waiting for proper REPORT integration https://github.com/perry-mitchell/webdav-client/issues/207
.then(res => {
response = res
return res.data
})
.then(parseXML)
.then(xml => processMultistatus(xml, true))
.then(comments => processResponsePayload(response, comments, true))
.then(response => response.data)

const responseData = await response.text()
const result = await parseXML(responseData)
const stat = getDirectoryFiles(result, true)
return processResponsePayload(response, stat, true)
}

// https://github.com/perry-mitchell/webdav-client/blob/9de2da4a2599e06bd86c2778145b7ade39fe0b3c/source/interface/directoryContents.js#L32
/**
* @param {any} result -
* @param {any} isDetailed -
*/
function processMultistatus(result, isDetailed = false) {
// https://github.com/perry-mitchell/webdav-client/blob/8d9694613c978ce7404e26a401c39a41f125f87f/source/operations/directoryContents.ts
const getDirectoryFiles = function(
result: DAVResult,
isDetailed = false,
): Array<FileStat> {
// Extract the response items (directory contents)
const {
multistatus: { response: responseItems },
} = result

// Map all items to a consistent output structure (results)
return responseItems.map(item => {
// Each item should contain a stat object
const {
propstat: { prop: props },
} = item
// Decode HTML entities
const decodedProps = {
...props,
// Decode twice to handle potentially double-encoded entities
// FIXME Remove this once https://github.com/nextcloud/server/issues/29306 is resolved
actorDisplayName: decodeHtmlEntities(props.actorDisplayName, 2),
message: decodeHtmlEntities(props.message, 2),
}
return prepareFileFromProps(decodedProps, decodedProps.id.toString(), isDetailed)

return prepareFileFromProps(props, props.id.toString(), isDetailed)
})
}
22 changes: 6 additions & 16 deletions apps/comments/src/utils/cancelableRequest.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,26 +20,15 @@
*
*/

import axios from '@nextcloud/axios'

/**
* Create a cancel token
*
* @return {import('axios').CancelTokenSource}
*/
const createCancelToken = () => axios.CancelToken.source()

/**
* Creates a cancelable axios 'request object'.
*
* @param {Function} request the axios promise request
* @return {object}
*/
const cancelableRequest = function(request) {
/**
* Generate an axios cancel token
*/
const cancelToken = createCancelToken()
const controller = new AbortController()
const signal = controller.signal

/**
* Execute the request
Expand All @@ -48,15 +37,16 @@ const cancelableRequest = function(request) {
* @param {object} [options] optional config for the request
*/
const fetch = async function(url, options) {
return request(
const response = await request(
url,
Object.assign({ cancelToken: cancelToken.token }, options)
Object.assign({ signal }, options)
)
return response
}

return {
request: fetch,
cancel: cancelToken.cancel,
abort: () => controller.abort(),
}
}

Expand Down
10 changes: 5 additions & 5 deletions apps/comments/src/views/Comments.vue
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ import MessageReplyTextIcon from 'vue-material-design-icons/MessageReplyText.vue
import AlertCircleOutlineIcon from 'vue-material-design-icons/AlertCircleOutline.vue'

import Comment from '../components/Comment.vue'
import getComments, { DEFAULT_LIMIT } from '../services/GetComments.js'
import { getComments, DEFAULT_LIMIT } from '../services/GetComments.ts'
import cancelableRequest from '../utils/cancelableRequest.js'

Vue.use(VTooltip)
Expand Down Expand Up @@ -206,14 +206,14 @@ export default {
this.error = ''

// Init cancellable request
const { request, cancel } = cancelableRequest(getComments)
this.cancelRequest = cancel
const { request, abort } = cancelableRequest(getComments)
this.cancelRequest = abort

// Fetch comments
const comments = await request({
const { data: comments } = await request({
commentsType: this.commentsType,
ressourceId: this.ressourceId,
}, { offset: this.offset })
}, { offset: this.offset }) || { data: [] }

this.logger.debug(`Processed ${comments.length} comments`, { comments })

Expand Down
2 changes: 1 addition & 1 deletion apps/dav/src/service/CalendarService.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
*/
import { getClient } from '../dav/client.js'
import logger from './logger.js'
import { parseXML } from 'webdav/dist/node/tools/dav.js'
import { parseXML } from 'webdav'

import {
slotsToVavailability,
Expand Down
5 changes: 5 additions & 0 deletions apps/files/appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,11 @@
'url' => '/directEditing/{token}',
'verb' => 'GET'
],
[
'name' => 'api#serviceWorker',
'url' => '/preview-service-worker.js',
'verb' => 'GET'
],
[
'name' => 'view#index',
'url' => '/{view}',
Expand Down
16 changes: 10 additions & 6 deletions apps/files/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@
* Initializes the files app
*/
initialize: function() {
this.navigation = OCP.Files.Navigation;
this.$showHiddenFiles = $('input#showhiddenfilesToggle');
var showHidden = $('#showHiddenFiles').val() === "1";
this.$showHiddenFiles.prop('checked', showHidden);
Expand Down Expand Up @@ -117,7 +116,9 @@
},
],
sorting: {
mode: $('#defaultFileSorting').val(),
mode: $('#defaultFileSorting').val() === 'basename'
? 'name'
: $('#defaultFileSorting').val(),
direction: $('#defaultFileSortingDirection').val()
},
config: this._filesConfig,
Expand All @@ -135,8 +136,6 @@
OC.Plugins.attach('OCA.Files.App', this);

this._setupEvents();
// trigger URL change event handlers
this._onPopState({ ...OC.Util.History.parseUrlQuery(), view: this.navigation?.active?.id });

this._debouncedPersistShowHiddenFilesState = _.debounce(this._persistShowHiddenFilesState, 1200);
this._debouncedPersistCropImagePreviewsState = _.debounce(this._persistCropImagePreviewsState, 1200);
Expand All @@ -145,6 +144,10 @@
OCP.WhatsNew.query(); // for Nextcloud server
sessionStorage.setItem('WhatsNewServerCheck', Date.now());
}

window._nc_event_bus.emit('files:legacy-view:initialized', this);

this.navigation = OCP.Files.Navigation
},

/**
Expand Down Expand Up @@ -225,7 +228,8 @@
* @return view id
*/
getActiveView: function() {
return this.navigation.active
return this.navigation
&& this.navigation.active
&& this.navigation.active.id;
},

Expand Down Expand Up @@ -314,7 +318,7 @@
view: 'files'
}, params);

var lastId = this.navigation.active;
var lastId = this.getActiveView();
if (!this.navigation.views.find(view => view.id === params.view)) {
params.view = 'files';
}
Expand Down
6 changes: 4 additions & 2 deletions apps/files/js/filelist.js
Original file line number Diff line number Diff line change
Expand Up @@ -2181,8 +2181,10 @@

if (persist && OC.getCurrentUser().uid) {
$.post(OC.generateUrl('/apps/files/api/v1/sorting'), {
mode: sort,
direction: direction
// Compatibility with new files-to-vue API
mode: sort === 'name' ? 'basename' : sort,
direction: direction,
view: 'files'
});
}
},
Expand Down
49 changes: 39 additions & 10 deletions apps/files/lib/Controller/ApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,12 @@
use OCA\Files\Service\UserConfig;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\ContentSecurityPolicy;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Http\FileDisplayResponse;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\Response;
use OCP\AppFramework\Http\StreamResponse;
use OCP\Files\File;
use OCP\Files\Folder;
use OCP\Files\NotFoundException;
Expand Down Expand Up @@ -279,20 +281,29 @@ public function getStorageStats($dir = '/'): JSONResponse {
*
* @param string $mode
* @param string $direction
* @return Response
* @return JSONResponse
* @throws \OCP\PreConditionNotMetException
*/
public function updateFileSorting($mode, $direction) {
$allowedMode = ['name', 'size', 'mtime'];
public function updateFileSorting($mode, string $direction = 'asc', string $view = 'files'): JSONResponse {
$allowedDirection = ['asc', 'desc'];
if (!in_array($mode, $allowedMode) || !in_array($direction, $allowedDirection)) {
$response = new Response();
$response->setStatus(Http::STATUS_UNPROCESSABLE_ENTITY);
return $response;
if (!in_array($direction, $allowedDirection)) {
return new JSONResponse(['message' => 'Invalid direction parameter'], Http::STATUS_UNPROCESSABLE_ENTITY);
}
$this->config->setUserValue($this->userSession->getUser()->getUID(), 'files', 'file_sorting', $mode);
$this->config->setUserValue($this->userSession->getUser()->getUID(), 'files', 'file_sorting_direction', $direction);
return new Response();

$userId = $this->userSession->getUser()->getUID();

Check notice

Code scanning / Psalm

PossiblyNullReference

Cannot call method getUID on possibly null value

$sortingJson = $this->config->getUserValue($userId, 'files', 'files_sorting_configs', '{}');
$sortingConfig = json_decode($sortingJson, true) ?: [];
$sortingConfig[$view] = [
'mode' => $mode,
'direction' => $direction,
];

$this->config->setUserValue($userId, 'files', 'files_sorting_configs', json_encode($sortingConfig));
return new JSONResponse([
'message' => 'ok',
'data' => $sortingConfig,
]);
}

/**
Expand Down Expand Up @@ -417,4 +428,22 @@ public function getNodeType($folderpath) {
$node = $this->userFolder->get($folderpath);
return $node->getType();
}

/**
* @NoAdminRequired
* @NoCSRFRequired
*/
public function serviceWorker(): StreamResponse {
$response = new StreamResponse(__DIR__ . '/../../../../dist/preview-service-worker.js');
$response->setHeaders([
'Content-Type' => 'application/javascript',
'Service-Worker-Allowed' => '/'
]);
$policy = new ContentSecurityPolicy();
$policy->addAllowedWorkerSrcDomain("'self'");
$policy->addAllowedScriptDomain("'self'");
$policy->addAllowedConnectDomain("'self'");
$response->setContentSecurityPolicy($policy);
return $response;
}
}
8 changes: 6 additions & 2 deletions apps/files/lib/Controller/ViewController.php
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,10 @@ public function index($dir = '', $view = '', $fileid = null, $fileNotFound = fal
$this->initialState->provideInitialState('navigation', $navItems);
$this->initialState->provideInitialState('config', $this->userConfig->getConfigs());

// File sorting user config
$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 = '';
Expand Down Expand Up @@ -292,8 +296,8 @@ public function index($dir = '', $view = '', $fileid = null, $fileNotFound = fal
$params['ownerDisplayName'] = $storageInfo['ownerDisplayName'] ?? '';
$params['isPublic'] = false;
$params['allowShareWithLink'] = $this->shareManager->shareApiAllowLinks() ? 'yes' : 'no';
$params['defaultFileSorting'] = $this->config->getUserValue($userId, 'files', 'file_sorting', 'name');
$params['defaultFileSortingDirection'] = $this->config->getUserValue($userId, 'files', 'file_sorting_direction', 'asc');
$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;
Expand Down
Loading