Skip to content
Merged
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
2 changes: 1 addition & 1 deletion appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Approve/reject files based on workflows defined by admins.
**Warning**: The DocuSign integration is no longer part of this app
and can be installed with [this app](https://apps.nextcloud.com/apps/integration_docusign).
]]></description>
<version>2.4.0</version>
<version>2.5.0</version>
<licence>agpl</licence>
<author>Julien Veyssier</author>
<namespace>Approval</namespace>
Expand Down
11 changes: 7 additions & 4 deletions lib/Controller/ApprovalController.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public function getApprovalState(int $fileId): DataResponse {
$state['userId'] = $activity['userId'];
$state['userName'] = $activity['userName'];
$state['timestamp'] = $activity['timestamp'];
$state['message'] = $activity['message'];
}
}
return new DataResponse($state);
Expand All @@ -74,23 +75,25 @@ public function getPendingNodes(?int $since = null): DataResponse {
* Approve a file
*
* @param int $fileId
* @param string|null $message
* @return DataResponse
*/
#[NoAdminRequired]
public function approve(int $fileId): DataResponse {
$this->approvalService->approve($fileId, $this->userId);
public function approve(int $fileId, ?string $message = ''): DataResponse {
$this->approvalService->approve($fileId, $this->userId, $message);
return new DataResponse(1);
}

/**
* Reject a file
*
* @param int $fileId
* @param string|null $message
* @return DataResponse
*/
#[NoAdminRequired]
public function reject(int $fileId): DataResponse {
$this->approvalService->reject($fileId, $this->userId);
public function reject(int $fileId, ?string $message = ''): DataResponse {
$this->approvalService->reject($fileId, $this->userId, $message);
return new DataResponse(1);
}

Expand Down
55 changes: 55 additions & 0 deletions lib/Migration/Version020400Date20250919105115.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Approval\Migration;

use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\DB\Types;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;

/**
* Auto-generated migration step: Please modify to your needs!
*/
class Version020400Date20250919105115 extends SimpleMigrationStep {

public function __construct(
) {
}

/**
* @param IOutput $output
* @param Closure(): ISchemaWrapper $schemaClosure
* @param array $options
*/
public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
}

/**
* @param IOutput $output
* @param Closure(): ISchemaWrapper $schemaClosure
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
if ($schema->hasTable('approval_activity')) {
$table = $schema->getTable('approval_activity');
if (!$table->hasColumn('message')) {
$table->addColumn('message', Types::TEXT, [
'notnull' => false, // OCI considers an empty string to be the same as a null value
]);
}
}

return $schema;
}
}
10 changes: 6 additions & 4 deletions lib/Service/ApprovalService.php
Original file line number Diff line number Diff line change
Expand Up @@ -339,9 +339,10 @@ public function getApprovalState(int $fileId, ?string $userId, bool $userHasAcce
*
* @param int $fileId
* @param string|null $userId
* @param string $message
* @return bool success
*/
public function approve(int $fileId, ?string $userId): bool {
public function approve(int $fileId, ?string $userId, string $message = ''): bool {
$fileState = $this->getApprovalState($fileId, $userId);
// if file has pending tag and user is authorized to approve it
if ($fileState['state'] === Application::STATE_APPROVABLE) {
Expand All @@ -354,7 +355,7 @@ public function approve(int $fileId, ?string $userId): bool {
$this->tagObjectMapper->unassignTags((string)$fileId, 'files', $rule['tagPending']);

// store activity in our tables
$this->ruleService->storeAction($fileId, $ruleId, $userId, Application::STATE_APPROVED);
$this->ruleService->storeAction($fileId, $ruleId, $userId, Application::STATE_APPROVED, $message);

$this->sendApprovalNotification($fileId, $userId, true);
$this->activityManager->triggerEvent(
Expand All @@ -376,9 +377,10 @@ public function approve(int $fileId, ?string $userId): bool {
*
* @param int $fileId
* @param string|null $userId
* @param string $message
* @return bool success
*/
public function reject(int $fileId, ?string $userId): bool {
public function reject(int $fileId, ?string $userId, string $message = ''): bool {
$fileState = $this->getApprovalState($fileId, $userId);
// if file has pending tag and user is authorized to approve it
if ($fileState['state'] === Application::STATE_APPROVABLE) {
Expand All @@ -391,7 +393,7 @@ public function reject(int $fileId, ?string $userId): bool {
$this->tagObjectMapper->unassignTags((string)$fileId, 'files', $rule['tagPending']);

// store activity in our tables
$this->ruleService->storeAction($fileId, $ruleId, $userId, Application::STATE_REJECTED);
$this->ruleService->storeAction($fileId, $ruleId, $userId, Application::STATE_REJECTED, $message);

$this->sendApprovalNotification($fileId, $userId, false);
$this->activityManager->triggerEvent(
Expand Down
5 changes: 4 additions & 1 deletion lib/Service/RuleService.php
Original file line number Diff line number Diff line change
Expand Up @@ -441,9 +441,10 @@ private function getRuleEntities(int $ruleId, string $role): array {
* @param int $ruleId
* @param string $userId
* @param int $newState
* @param string $message
* @return void
*/
public function storeAction(int $fileId, int $ruleId, string $userId, int $newState): void {
public function storeAction(int $fileId, int $ruleId, string $userId, int $newState, string $message = ''): void {
$qb = $this->db->getQueryBuilder();
$qb->delete('approval_activity')
->where(
Expand All @@ -463,6 +464,7 @@ public function storeAction(int $fileId, int $ruleId, string $userId, int $newSt
'user_id' => $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR),
'new_state' => $qb->createNamedParameter($newState, IQueryBuilder::PARAM_INT),
'timestamp' => $qb->createNamedParameter($timestamp, IQueryBuilder::PARAM_INT),
'message' => $qb->createNamedParameter($message, IQueryBuilder::PARAM_STR),
]);
$qb->executeStatement();
$qb->resetQueryParts();
Expand Down Expand Up @@ -497,6 +499,7 @@ public function getLastAction(int $fileId, int $ruleId, int $newState): ?array {
$activity = [
'userId' => $row['user_id'],
'timestamp' => (int)$row['timestamp'],
'message' => $row['message'] ?? '',
];
break;
}
Expand Down
26 changes: 24 additions & 2 deletions src/components/Info.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
)
}}
</div>
<NcInputField v-if="stateApprovable" v-model="newMessage" :label="t('approval', 'Reason (optional)')" />
<ApprovalButtons
v-if="stateApprovable"
class="buttons"
Expand All @@ -41,6 +42,9 @@
</span>
<span v-else>{{ rejectedText }}</span>
</span>
<span v-if="message" class="state-label">
<MessageDrawIcon />{{ messageText }}
</span>
<span v-if="statePending" class="state-label pending-label">
<DotsHorizontalCircleIcon class="pending" :size="32" />
<span v-if="userId && timestamp" class="details">
Expand All @@ -67,9 +71,11 @@ import CheckIcon from 'vue-material-design-icons/Check.vue'
import CheckCircleIcon from 'vue-material-design-icons/CheckCircle.vue'
import DotsHorizontalCircleIcon from 'vue-material-design-icons/DotsHorizontalCircle.vue'
import CloseCircleIcon from 'vue-material-design-icons/CloseCircle.vue'
import MessageDrawIcon from 'vue-material-design-icons/MessageDraw.vue'

import NcButton from '@nextcloud/vue/components/NcButton'
import NcUserBubble from '@nextcloud/vue/components/NcUserBubble'
import NcInputField from '@nextcloud/vue/components/NcInputField'

import ApprovalButtons from './ApprovalButtons.vue'

Expand All @@ -85,10 +91,12 @@ export default {
ApprovalButtons,
NcButton,
NcUserBubble,
NcInputField,
CheckCircleIcon,
CloseCircleIcon,
CheckIcon,
DotsHorizontalCircleIcon,
MessageDrawIcon,
},

props: {
Expand All @@ -100,6 +108,10 @@ export default {
type: [Number, null],
default: null,
},
message: {
type: String,
default: '',
},
userName: {
type: [String, null],
default: null,
Expand Down Expand Up @@ -143,6 +155,7 @@ export default {
data() {
return {
you: t('approval', 'you'),
newMessage: '',
}
},

Expand Down Expand Up @@ -218,6 +231,15 @@ export default {
? t('approval', 'Approval requested by {user}', { user: this.userName })
: t('approval', 'Approval requested by you')
},
messageText() {
if (this.stateApproved) {
return t('approval', 'Reason for approval: {message}', { message: this.message })
}
if (this.stateRejected) {
return t('approval', 'Reason for rejection: {message}', { message: this.message })
}
return this.message
},
},

watch: {},
Expand All @@ -226,10 +248,10 @@ export default {

methods: {
onApprove() {
this.$emit('approve')
this.$emit('approve', this.newMessage)
},
onReject() {
this.$emit('reject')
this.$emit('reject', this.newMessage)
},
onRequest() {
this.$emit('request')
Expand Down
8 changes: 4 additions & 4 deletions src/components/InfoModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,13 @@ export default {
setUserRules(rules) {
this.userRules = rules
},
onApprove() {
onApprove(message) {
this.closeModal()
this.$emit('approve', this.node)
this.$emit('approve', this.node, message)
},
onReject() {
onReject(message) {
this.closeModal()
this.$emit('reject', this.node)
this.$emit('reject', this.node, message)
},
onRequest() {
this.closeModal()
Expand Down
1 change: 1 addition & 0 deletions src/files/actions/approveAction.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const approveAction = new FileAction({
return !OCA.Approval.actionIgnoreLists.includes(view.id)
&& !nodes.some(({ permissions }) => (permissions & Permission.READ) === 0)
&& nodes.some(node => node.attributes['approval-state'] === states.APPROVABLE)
&& nodes.length > 1
// && nodes.every(({ type }) => type === FileType.File)
// && nodes.every(({ mime }) => mime === 'application/some+type')
},
Expand Down
2 changes: 1 addition & 1 deletion src/files/actions/inlineAction.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const inlineAction = new FileAction({
return state === states.PENDING
? t('approval', 'Waiting for authorized users to approve this file')
: state === states.APPROVABLE
? t('approval', 'Pending approval, you are authorized to approve')
? t('approval', 'Pending approval, click to approve/reject')
: state === states.APPROVED
? t('approval', 'This element was approved')
: t('approval', 'This element was rejected')
Expand Down
1 change: 1 addition & 0 deletions src/files/actions/rejectAction.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const rejectAction = new FileAction({
return !OCA.Approval.actionIgnoreLists.includes(view.id)
&& !nodes.some(({ permissions }) => (permissions & Permission.READ) === 0)
&& nodes.some(node => node.attributes['approval-state'] === states.APPROVABLE)
&& nodes.length > 1
// && nodes.every(({ type }) => type === FileType.File)
// && nodes.every(({ mime }) => mime === 'application/some+type')
},
Expand Down
37 changes: 37 additions & 0 deletions src/files/actions/respondAction.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import PendingIconSvg from '@mdi/svg/svg/dots-horizontal-circle-outline.svg?raw'
import { Permission, FileAction } from '@nextcloud/files'
import { states } from '../../states.js'
import { openApprovalInfoModal, updateNodeApprovalState } from '../helpers.js'

export const respondAction = new FileAction({
id: 'approval-respond',
displayName: (nodes) => {
return t('approval', 'Approve or Reject')
},
enabled(nodes, view) {
return !OCA.Approval.actionIgnoreLists.includes(view.id)
&& !nodes.some(({ permissions }) => (permissions & Permission.READ) === 0)
&& nodes.some(node => node.attributes['approval-state'] === states.APPROVABLE)
&& nodes.length === 1
// && nodes.every(({ type }) => type === FileType.File)
// && nodes.every(({ mime }) => mime === 'application/some+type')
},
iconSvgInline: () => PendingIconSvg,
order: 0,
async exec(node) {
try {
await updateNodeApprovalState(node)
await openApprovalInfoModal(node)
} catch (error) {
console.debug('Approve or Reject action failed')
}
return null
},
async execBatch(nodes) {
},
})
8 changes: 4 additions & 4 deletions src/files/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,10 @@ export async function requestAfterShareCreation(fileId, fileName, ruleId, node =
}
}

export async function approve(fileId, fileName, node = null, notify = true) {
export async function approve(fileId, fileName, node = null, notify = true, message = '') {
const url = generateOcsUrl('apps/approval/api/v1/approve/{fileId}', { fileId })
try {
await axios.put(url)
await axios.put(url, { message })
if (notify) {
showSuccess(t('approval', 'You approved {name}', { name: fileName }))
}
Expand All @@ -102,10 +102,10 @@ export async function approve(fileId, fileName, node = null, notify = true) {
}
}

export async function reject(fileId, fileName, node = null, notify = true) {
export async function reject(fileId, fileName, node = null, notify = true, message = '') {
const url = generateOcsUrl('apps/approval/api/v1/reject/{fileId}', { fileId })
try {
await axios.put(url)
await axios.put(url, { message })
if (notify) {
showSuccess(t('approval', 'You rejected {name}', { name: fileName }))
}
Expand Down
2 changes: 2 additions & 0 deletions src/files/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { registerFileAction, registerDavProperty } from '@nextcloud/files'

import { inlineAction } from './actions/inlineAction.js'
import { requestAction } from './actions/requestAction.js'
import { respondAction } from './actions/respondAction.js'
import { approveAction } from './actions/approveAction.js'
import { rejectAction } from './actions/rejectAction.js'

Expand All @@ -24,3 +25,4 @@ registerFileAction(inlineAction)
registerFileAction(approveAction)
registerFileAction(rejectAction)
registerFileAction(requestAction)
registerFileAction(respondAction)
8 changes: 4 additions & 4 deletions src/files/modals.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@ export function createInfoModal() {
onClose: () => {
console.debug('[Approval] modal closed')
},
onApprove: (node) => {
approve(node.fileid, node.basename, node)
onApprove: (node, message) => {
approve(node.fileid, node.basename, node, true, message)
},
onReject: (node) => {
reject(node.fileid, node.basename, node)
onReject: (node, message) => {
reject(node.fileid, node.basename, node, true, message)
},
onRequest: (node) => {
onRequestFileAction(node)
Expand Down
Loading
Loading