Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
feat(files): propagate restore and delete events
Signed-off-by: John Molakvoæ <[email protected]>
  • Loading branch information
skjnldsv committed Apr 6, 2023
commit f28944e23f96dd756cba3739e99c2fba57e81f1f
15 changes: 10 additions & 5 deletions apps/files/src/actions/deleteAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,28 +19,33 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import { registerFileAction, Permission, FileAction } from '@nextcloud/files'
import { registerFileAction, Permission, FileAction, Node } from '@nextcloud/files'
import { translate as t } from '@nextcloud/l10n'
import axios from '@nextcloud/axios'
import TrashCan from '@mdi/svg/svg/trash-can.svg?raw'
import logger from '../logger'
import { emit } from '@nextcloud/event-bus'

registerFileAction(new FileAction({
id: 'delete',
displayName(nodes, view) {
displayName(nodes: Node[], view) {
return view.id === 'trashbin'
? t('files_trashbin', 'Delete permanently')
: t('files', 'Delete')
},
iconSvgInline: () => TrashCan,
enabled(nodes) {
enabled(nodes: Node[]) {
return nodes.length > 0 && nodes
.map(node => node.permissions)
.every(permission => (permission & Permission.DELETE) !== 0)
},
async exec(node) {
async exec(node: Node) {
// No try...catch here, let the files app handle the error
await axios.delete(node.source)

// Let's delete even if it's moved to the trashbin
// since it has been removed from the current view
// and changing the view will trigger a reload anyway.
emit('files:file:deleted', node)
return true
},
order: 100,
Expand Down
10 changes: 10 additions & 0 deletions apps/files/src/components/FileEntry.vue
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,16 @@ export default Vue.extend({
<style scoped lang='scss'>
@import '../mixins/fileslist-row.scss';

/* Hover effect on tbody lines only */
tr {
&:hover,
&:focus,
&:active {
background-color: var(--color-background-dark);
}
}

/* Preview not loaded animation effect */
.files-list__row-icon-preview:not([style*='background']) {
background: linear-gradient(110deg, var(--color-loading-dark) 0%, var(--color-loading-dark) 25%, var(--color-loading-light) 50%, var(--color-loading-dark) 75%, var(--color-loading-dark) 100%);
background-size: 400%;
Expand Down
14 changes: 7 additions & 7 deletions apps/files/src/components/FilesListVirtual.vue
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ export default Vue.extend({
height: 100%;

&::v-deep {
// Table head, body and footer
tbody, .vue-recycle-scroller__slot {
display: flex;
flex-direction: column;
Expand All @@ -148,7 +149,7 @@ export default Vue.extend({
}

// Table header
.vue-recycle-scroller__slot {
.vue-recycle-scroller__slot[role='thead'] {
// Pinned on top when scrolling
position: sticky;
z-index: 10;
Expand All @@ -157,18 +158,17 @@ export default Vue.extend({
background-color: var(--color-main-background);
}

/**
* Common row styling. tr are handled by
* vue-virtual-scroller, so we need to
* have those rules in here.
*/
tr {
position: absolute;
display: flex;
align-items: center;
width: 100%;
border-bottom: 1px solid var(--color-border);

&:hover,
&:focus,
&:active {
background-color: var(--color-background-dark);
}
}
}
}
Expand Down
115 changes: 78 additions & 37 deletions apps/files/src/store/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,51 +24,92 @@ import type { Folder, Node } from '@nextcloud/files'
import type { FilesStore, RootsStore, RootOptions, Service, FilesState } from '../types'

import { defineStore } from 'pinia'
import { subscribe } from '@nextcloud/event-bus'
import Vue from 'vue'
import logger from '../logger'

export const useFilesStore = defineStore('files', {
state: (): FilesState => ({
files: {} as FilesStore,
roots: {} as RootsStore,
}),
export const useFilesStore = () => {
const store = defineStore('files', {
state: (): FilesState => ({
files: {} as FilesStore,
roots: {} as RootsStore,
}),

getters: {
/**
* Get a file or folder by id
*/
getNode: (state) => (id: number): Node|undefined => state.files[id],
getters: {
/**
* Get a file or folder by id
*/
getNode: (state) => (id: number): Node|undefined => state.files[id],

/**
* Get a list of files or folders by their IDs
* Does not return undefined values
*/
getNodes: (state) => (ids: number[]): Node[] => ids
.map(id => state.files[id])
.filter(Boolean),
/**
* Get a file or folder by id
*/
getRoot: (state) => (service: Service): Folder|undefined => state.roots[service],
},
/**
* Get a list of files or folders by their IDs
* Does not return undefined values
*/
getNodes: (state) => (ids: number[]): Node[] => ids
.map(id => state.files[id])
.filter(Boolean),
/**
* Get a file or folder by id
*/
getRoot: (state) => (service: Service): Folder|undefined => state.roots[service],
},

actions: {
updateNodes(nodes: Node[]) {
// Update the store all at once
const files = nodes.reduce((acc, node) => {
if (!node.attributes.fileid) {
logger.warn('Trying to update/set a node without fileid', node)
actions: {
updateNodes(nodes: Node[]) {
// Update the store all at once
const files = nodes.reduce((acc, node) => {
if (!node.attributes.fileid) {
logger.warn('Trying to update/set a node without fileid', node)
return acc
}
acc[node.attributes.fileid] = node
return acc
}
acc[node.attributes.fileid] = node
return acc
}, {} as FilesStore)
}, {} as FilesStore)

Vue.set(this, 'files', {...this.files, ...files})
},
Vue.set(this, 'files', {...this.files, ...files})
},

deleteNodes(nodes: Node[]) {
nodes.forEach(node => {
if (node.fileid) {
Vue.delete(this.files, node.fileid)
}
})
},

setRoot({ service, root }: RootOptions) {
Vue.set(this.roots, service, root)
},

onCreatedNode() {
// TODO: do something
},

setRoot({ service, root }: RootOptions) {
Vue.set(this.roots, service, root)
onDeletedNode(node: Node) {
this.deleteNodes([node])
},

onMovedNode() {
// TODO: do something
},
}
})

const fileStore = store()
// Make sure we only register the listeners once
if (!fileStore.initialized) {
subscribe('files:file:created', fileStore.onCreatedNode)
subscribe('files:file:deleted', fileStore.onDeletedNode)
subscribe('files:file:moved', fileStore.onMovedNode)
// subscribe('files:file:updated', fileStore.onUpdatedNode)

subscribe('files:folder:created', fileStore.onCreatedNode)
subscribe('files:folder:deleted', fileStore.onDeletedNode)
subscribe('files:folder:moved', fileStore.onMovedNode)
// subscribe('files:folder:updated', fileStore.onUpdatedNode)

fileStore.initialized = true
}
})

return fileStore
}
56 changes: 36 additions & 20 deletions apps/files/src/store/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,30 +24,46 @@ import type { PathOptions, ServicesState } from '../types'

import { defineStore } from 'pinia'
import Vue from 'vue'
import { subscribe } from '@nextcloud/event-bus'

export const usePathsStore = defineStore('paths', {
state: (): ServicesState => ({}),
export const usePathsStore = () => {
const store = defineStore('paths', {
state: (): ServicesState => ({}),

getters: {
getPath: (state) => {
return (service: string, path: string): number|undefined => {
if (!state[service]) {
return undefined
getters: {
getPath: (state) => {
return (service: string, path: string): number|undefined => {
if (!state[service]) {
return undefined
}
return state[service][path]
}
return state[service][path]
}
},
},
},

actions: {
addPath(payload: PathOptions) {
// If it doesn't exists, init the service state
if (!this[payload.service]) {
Vue.set(this, payload.service, {})
}
actions: {
addPath(payload: PathOptions) {
// If it doesn't exists, init the service state
if (!this[payload.service]) {
Vue.set(this, payload.service, {})
}

// Now we can set the provided path
Vue.set(this[payload.service], payload.path, payload.fileid)
},
// Now we can set the provided path
Vue.set(this[payload.service], payload.path, payload.fileid)
},
}
})

const pathsStore = store()
// Make sure we only register the listeners once
if (!pathsStore.initialized) {
// TODO: watch folders to update paths?
// subscribe('files:folder:created', pathsStore.onCreatedNode)
// subscribe('files:folder:deleted', pathsStore.onDeletedNode)
// subscribe('files:folder:moved', pathsStore.onMovedNode)

pathsStore.initialized = true
}
})

return pathsStore
}
13 changes: 1 addition & 12 deletions apps/files/src/store/sorting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,7 @@ import { generateUrl } from '@nextcloud/router'
import { defineStore } from 'pinia'
import Vue from 'vue'
import axios from '@nextcloud/axios'

type direction = 'asc' | 'desc'

interface SortingConfig {
mode: string
direction: direction
}

interface SortingStore {
[key: string]: SortingConfig
}
import type { direction, SortingStore } from '../types'

const saveUserConfig = (mode: string, direction: direction, view: string) => {
return axios.post(generateUrl('/apps/files/api/v1/sorting'), {
Expand All @@ -46,7 +36,6 @@ const saveUserConfig = (mode: string, direction: direction, view: string) => {
}

const filesSortingConfig = loadState('files', 'filesSortingConfig', {}) as SortingStore
console.debug('filesSortingConfig', filesSortingConfig)

export const useSortingStore = defineStore('sorting', {
state: () => ({
Expand Down
12 changes: 12 additions & 0 deletions apps/files/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,15 @@ export interface PathOptions {
path: string
fileid: number
}

// Sorting store
export type direction = 'asc' | 'desc'

export interface SortingConfig {
mode: string
direction: direction
}

export interface SortingStore {
[key: string]: SortingConfig
}
4 changes: 2 additions & 2 deletions apps/files/src/views/FilesList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -173,13 +173,13 @@ 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)]
const results = [...(this.currentFolder?.children || []).map(this.getNode).filter(file => file)]
.sort(customColumn.sort)
return this.isAscSorting ? results : results.reverse()
}

return orderBy(
[...(this.currentFolder?.children || []).map(this.getNode)],
[...(this.currentFolder?.children || []).map(this.getNode).filter(file => file)],
[
// Sort folders first if sorting by name
...this.sortingMode === 'basename' ? [v => v.type !== 'folder'] : [],
Expand Down
14 changes: 10 additions & 4 deletions apps/files_trashbin/src/actions/restoreAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,21 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import { registerFileAction, Permission, FileAction } from '@nextcloud/files'
import { registerFileAction, Permission, FileAction, Node } from '@nextcloud/files'
import { translate as t } from '@nextcloud/l10n'
import axios from '@nextcloud/axios'
import History from '@mdi/svg/svg/history.svg?raw'
import { generateRemoteUrl } from '@nextcloud/router'
import { getCurrentUser } from '@nextcloud/auth'
import { emit } from '@nextcloud/event-bus'

registerFileAction(new FileAction({
id: 'restore',
displayName() {
return t('files_trashbin', 'Restore')
},
iconSvgInline: () => History,
enabled(nodes, view) {
enabled(nodes: Node[], view) {
// Only available in the trashbin view
if (view.id !== 'trashbin') {
return false
Expand All @@ -43,15 +44,20 @@ registerFileAction(new FileAction({
.map(node => node.permissions)
.every(permission => (permission & Permission.READ) !== 0)
},
async exec(node) {
async exec(node: Node) {
// No try...catch here, let the files app handle the error
const destination = generateRemoteUrl(`dav/trashbin/${getCurrentUser()?.uid}/restore/${node.basename}`)
await axios({
method: 'MOVE',
url: node.source,
headers: {
destination: generateRemoteUrl(`dav/trashbin/${getCurrentUser()?.uid}/restore/${node.basename}`),
destination,
},
})

// Let's pretend the file is deleted since
// we don't know the restored location
emit('files:file:deleted', node)
return true
},
order: 1,
Expand Down