Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
293 changes: 221 additions & 72 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
"@nextcloud/vue": "^8.29.2",
"vue": "^2.7.16",
"vue-material-design-icons": "^5.3.1",
"vuex": "^3.6.2"
"vuex": "^3.6.2",
"webdav": "^5.8.0"
},
"engines": {
"node": "^22.0.0",
Expand All @@ -36,7 +37,10 @@
"@nextcloud/browserslist-config": "^3.0.1",
"@nextcloud/eslint-config": "^8.4.2",
"@nextcloud/stylelint-config": "^3.1.0",
"@nextcloud/typings": "^1.9.1",
"@nextcloud/webpack-vue-config": "^6.3.0",
"vue-template-compiler": "^2.7.16"
"ts-loader": "^9.5.4",
"typescript": "^5.9.2",
"vue-loader": "^15.11.1"
}
}
11 changes: 7 additions & 4 deletions src/AdminSettings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@
<th class="retention-heading__after">
{{ t('files_retention','From date of') }}
</th>
<th class="retention-heading__action"></th>
<th class="retention-heading__action" />
</tr>
</thead>
<tbody>
<RetentionRule v-for="rule in retentionRules"
:key="rule.id"
:tags="tags"
v-bind="rule">
{{ rule.tagid }}
</RetentionRule>
Expand Down Expand Up @@ -99,6 +100,7 @@ import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'

import RetentionRule from './Components/RetentionRule.vue'
import { fetchTags } from './services/api.ts'

import { showError, showSuccess, showWarning } from '@nextcloud/dialogs'
import { loadState } from '@nextcloud/initial-state'
Expand Down Expand Up @@ -145,6 +147,7 @@ export default {
filterAvailableTagList: (tag) => {
return !this.tagIdsWithRule.includes(tag.id)
},
tags: [],
}
},

Expand Down Expand Up @@ -172,7 +175,7 @@ export default {

async mounted() {
try {
await OC.SystemTags.collection.fetch({})
this.tags = await fetchTags()
await this.$store.dispatch('loadRetentionRules')

this.resetForm()
Expand Down Expand Up @@ -208,7 +211,7 @@ export default {
this.loadingNotifyBefore = false
showError(t('files_retention', 'An error occurred while changing the setting'))
}.bind(this),
}
},
)
},

Expand All @@ -225,7 +228,7 @@ export default {
return
}

const tagName = OC.SystemTags.collection.get(newTag)?.attributes?.name
const tagName = this.tags.find((tag) => tag.id === newTag)?.displayName

if (this.tagIdsWithRule.includes(newTag)) {
showError(t('files_retention', 'Tag {tagName} already has a retention rule', { tagName }))
Expand Down
6 changes: 5 additions & 1 deletion src/Components/RetentionRule.vue
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,15 @@ export default {
type: Boolean,
required: true,
},
tags: {
type: Array,
required: true,
},
},

computed: {
tagName() {
return OC.SystemTags.collection.get(this.tagid)?.attributes?.name
return this.tags.find((tag) => tag.id === this.tagid)?.displayName
},

getAmountAndUnit() {
Expand Down
11 changes: 11 additions & 0 deletions src/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { getLoggerBuilder } from '@nextcloud/logger'

export default getLoggerBuilder()
.setApp('files_retention')
.detectUser()
.build()
41 changes: 41 additions & 0 deletions src/services/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import type { FileStat, ResponseDataDetailed } from 'webdav'
import type { TagWithId } from '../types'

import { t } from '@nextcloud/l10n'

import { davClient } from './davClient'
import { parseTags } from '../utils'
import logger from '../logger'

export const fetchTagsPayload = `<?xml version="1.0"?>
<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns">
<d:prop>
<oc:id />
<oc:display-name />
<oc:user-visible />
<oc:user-assignable />
<oc:can-assign />
<d:getetag />
<nc:color />
</d:prop>
</d:propfind>`

export const fetchTags = async (): Promise<TagWithId[]> => {
const path = '/systemtags'
try {
const { data: tags } = await davClient.getDirectoryContents(path, {
data: fetchTagsPayload,
details: true,
glob: '/systemtags/*', // Filter out first empty tag
}) as ResponseDataDetailed<Required<FileStat>[]>
return parseTags(tags)
} catch (error) {
logger.error(t('files_retention', 'Failed to load tags'), { error })
throw new Error(t('files_retention', 'Failed to load tags'))
}
}
26 changes: 26 additions & 0 deletions src/services/davClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { createClient } from 'webdav'
import { generateRemoteUrl } from '@nextcloud/router'
import { getRequestToken, onRequestTokenUpdate } from '@nextcloud/auth'

// init webdav client
const rootUrl = generateRemoteUrl('dav')
export const davClient = createClient(rootUrl)

// set CSRF token header
const setHeaders = (token: string | null) => {
davClient.setHeaders({
// Add this so the server knows it is an request from the browser
'X-Requested-With': 'XMLHttpRequest',
// Inject user auth
requesttoken: token ?? '',
})
}

// refresh headers when request token changes
onRequestTokenUpdate(setHeaders)
setHeaders(getRequestToken())
23 changes: 23 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

export interface BaseTag {
id?: number
userVisible: boolean
userAssignable: boolean
readonly canAssign: boolean // Computed server-side
etag?: string
color?: string
}

export type Tag = BaseTag & {
displayName: string
}

export type TagWithId = Required<Tag>

export type ServerTag = BaseTag & {
name: string
}
23 changes: 23 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import camelCase from 'camelcase'

import type { DAVResultResponseProps } from 'webdav'

import type { BaseTag, TagWithId } from './types.js'

export const defaultBaseTag: BaseTag = {
userVisible: true,
userAssignable: true,
canAssign: true,
}

export const parseTags = (tags: { props: DAVResultResponseProps }[]): TagWithId[] => {
return tags.map(({ props }) => Object.fromEntries(
Object.entries(props)
.map(([key, value]) => [camelCase(key), camelCase(key) === 'displayName' ? String(value) : value]),
)) as TagWithId[]
}
19 changes: 19 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"include": ["src/**/*.ts", "src/**/*.vue", "src/env.d.ts"],
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "bundler",
"target": "ESNext",
"strictNullChecks": true,
"sourceMap": true,
"allowSyntheticDefaultImports": true,
"declaration": false,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"vueCompilerOptions": {
"target": "2.7"
}
}
2 changes: 2 additions & 0 deletions tsconfig.json.license
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
SPDX-License-Identifier: AGPL-3.0-or-later