mirror of
https://github.com/LibreSign/libresign.git
synced 2025-12-17 21:12:16 +01:00
Merge pull request #6231 from LibreSign/feat/signing-order-visual-diagram
feat: signing order visual diagram
This commit is contained in:
commit
45806e18d7
20 changed files with 738 additions and 46 deletions
|
|
@ -95,7 +95,7 @@ class PageController extends AEnvironmentPageAwareController {
|
||||||
|
|
||||||
$this->provideSignerSignatues();
|
$this->provideSignerSignatues();
|
||||||
$this->initialState->provideInitialState('identify_methods', $this->identifyMethodService->getIdentifyMethodsSettings());
|
$this->initialState->provideInitialState('identify_methods', $this->identifyMethodService->getIdentifyMethodsSettings());
|
||||||
$this->initialState->provideInitialState('signature_flow', $this->appConfig->getValueString(Application::APP_ID, 'signature_flow', \OCA\Libresign\Enum\SignatureFlow::PARALLEL->value));
|
$this->initialState->provideInitialState('signature_flow', $this->appConfig->getValueString(Application::APP_ID, 'signature_flow', \OCA\Libresign\Enum\SignatureFlow::NONE->value));
|
||||||
$this->initialState->provideInitialState('legal_information', $this->appConfig->getValueString(Application::APP_ID, 'legal_information'));
|
$this->initialState->provideInitialState('legal_information', $this->appConfig->getValueString(Application::APP_ID, 'legal_information'));
|
||||||
|
|
||||||
Util::addScript(Application::APP_ID, 'libresign-main');
|
Util::addScript(Application::APP_ID, 'libresign-main');
|
||||||
|
|
|
||||||
|
|
@ -136,6 +136,7 @@ class RequestSignatureController extends AEnvironmentAwareController {
|
||||||
* @param LibresignVisibleElement[]|null $visibleElements Visible elements on document
|
* @param LibresignVisibleElement[]|null $visibleElements Visible elements on document
|
||||||
* @param LibresignNewFile|array<empty>|null $file File object.
|
* @param LibresignNewFile|array<empty>|null $file File object.
|
||||||
* @param integer|null $status Numeric code of status * 0 - no signers * 1 - signed * 2 - pending
|
* @param integer|null $status Numeric code of status * 0 - no signers * 1 - signed * 2 - pending
|
||||||
|
* @param string|null $signatureFlow Signature flow mode: 'parallel' or 'ordered_numeric'. If not provided, uses global configuration
|
||||||
* @return DataResponse<Http::STATUS_OK, array{message: string, data: LibresignValidateFile}, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, array{message?: string, action?: integer, errors?: list<array{message: string, title?: string}>}, array{}>
|
* @return DataResponse<Http::STATUS_OK, array{message: string, data: LibresignValidateFile}, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, array{message?: string, action?: integer, errors?: list<array{message: string, title?: string}>}, array{}>
|
||||||
*
|
*
|
||||||
* 200: OK
|
* 200: OK
|
||||||
|
|
@ -145,7 +146,14 @@ class RequestSignatureController extends AEnvironmentAwareController {
|
||||||
#[NoCSRFRequired]
|
#[NoCSRFRequired]
|
||||||
#[RequireManager]
|
#[RequireManager]
|
||||||
#[ApiRoute(verb: 'PATCH', url: '/api/{apiVersion}/request-signature', requirements: ['apiVersion' => '(v1)'])]
|
#[ApiRoute(verb: 'PATCH', url: '/api/{apiVersion}/request-signature', requirements: ['apiVersion' => '(v1)'])]
|
||||||
public function updateSign(?array $users = [], ?string $uuid = null, ?array $visibleElements = null, ?array $file = [], ?int $status = null): DataResponse {
|
public function updateSign(
|
||||||
|
?array $users = [],
|
||||||
|
?string $uuid = null,
|
||||||
|
?array $visibleElements = null,
|
||||||
|
?array $file = [],
|
||||||
|
?int $status = null,
|
||||||
|
?string $signatureFlow = null,
|
||||||
|
): DataResponse {
|
||||||
$user = $this->userSession->getUser();
|
$user = $this->userSession->getUser();
|
||||||
$data = [
|
$data = [
|
||||||
'uuid' => $uuid,
|
'uuid' => $uuid,
|
||||||
|
|
@ -153,7 +161,8 @@ class RequestSignatureController extends AEnvironmentAwareController {
|
||||||
'users' => $users,
|
'users' => $users,
|
||||||
'userManager' => $user,
|
'userManager' => $user,
|
||||||
'status' => $status,
|
'status' => $status,
|
||||||
'visibleElements' => $visibleElements
|
'visibleElements' => $visibleElements,
|
||||||
|
'signatureFlow' => $signatureFlow,
|
||||||
];
|
];
|
||||||
try {
|
try {
|
||||||
$this->validateHelper->validateExistingFile($data);
|
$this->validateHelper->validateExistingFile($data);
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ class File extends Entity {
|
||||||
protected ?string $callback = null;
|
protected ?string $callback = null;
|
||||||
protected ?array $metadata = null;
|
protected ?array $metadata = null;
|
||||||
protected int $modificationStatus = 0;
|
protected int $modificationStatus = 0;
|
||||||
protected int $signatureFlow = SignatureFlow::NUMERIC_PARALLEL;
|
protected int $signatureFlow = SignatureFlow::NUMERIC_NONE;
|
||||||
protected int $docmdpLevel = 0;
|
protected int $docmdpLevel = 0;
|
||||||
public const STATUS_NOT_LIBRESIGN_FILE = -1;
|
public const STATUS_NOT_LIBRESIGN_FILE = -1;
|
||||||
public const STATUS_DRAFT = 0;
|
public const STATUS_DRAFT = 0;
|
||||||
|
|
|
||||||
|
|
@ -13,14 +13,17 @@ namespace OCA\Libresign\Enum;
|
||||||
* Signature flow modes
|
* Signature flow modes
|
||||||
*/
|
*/
|
||||||
enum SignatureFlow: string {
|
enum SignatureFlow: string {
|
||||||
|
case NONE = 'none';
|
||||||
case PARALLEL = 'parallel';
|
case PARALLEL = 'parallel';
|
||||||
case ORDERED_NUMERIC = 'ordered_numeric';
|
case ORDERED_NUMERIC = 'ordered_numeric';
|
||||||
|
|
||||||
|
public const NUMERIC_NONE = 0;
|
||||||
public const NUMERIC_PARALLEL = 1;
|
public const NUMERIC_PARALLEL = 1;
|
||||||
public const NUMERIC_ORDERED_NUMERIC = 2;
|
public const NUMERIC_ORDERED_NUMERIC = 2;
|
||||||
|
|
||||||
public function toNumeric(): int {
|
public function toNumeric(): int {
|
||||||
return match($this) {
|
return match($this) {
|
||||||
|
self::NONE => self::NUMERIC_NONE,
|
||||||
self::PARALLEL => self::NUMERIC_PARALLEL,
|
self::PARALLEL => self::NUMERIC_PARALLEL,
|
||||||
self::ORDERED_NUMERIC => self::NUMERIC_ORDERED_NUMERIC,
|
self::ORDERED_NUMERIC => self::NUMERIC_ORDERED_NUMERIC,
|
||||||
};
|
};
|
||||||
|
|
@ -28,6 +31,7 @@ enum SignatureFlow: string {
|
||||||
|
|
||||||
public static function fromNumeric(int $value): self {
|
public static function fromNumeric(int $value): self {
|
||||||
return match($value) {
|
return match($value) {
|
||||||
|
self::NUMERIC_NONE => self::NONE,
|
||||||
self::NUMERIC_PARALLEL => self::PARALLEL,
|
self::NUMERIC_PARALLEL => self::PARALLEL,
|
||||||
self::NUMERIC_ORDERED_NUMERIC => self::ORDERED_NUMERIC,
|
self::NUMERIC_ORDERED_NUMERIC => self::ORDERED_NUMERIC,
|
||||||
default => throw new \ValueError("Invalid numeric value for SignatureFlow: $value"),
|
default => throw new \ValueError("Invalid numeric value for SignatureFlow: $value"),
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ class TemplateLoader implements IEventListener {
|
||||||
|
|
||||||
$this->initialState->provideInitialState(
|
$this->initialState->provideInitialState(
|
||||||
'signature_flow',
|
'signature_flow',
|
||||||
$this->appConfig->getValueString(Application::APP_ID, 'signature_flow', \OCA\Libresign\Enum\SignatureFlow::PARALLEL->value)
|
$this->appConfig->getValueString(Application::APP_ID, 'signature_flow', \OCA\Libresign\Enum\SignatureFlow::NONE->value)
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -363,8 +363,8 @@ abstract class AEngineHandler implements IEngineHandler {
|
||||||
}
|
}
|
||||||
$pkiDirName = $this->caIdentifierService->generatePkiDirectoryName($caId);
|
$pkiDirName = $this->caIdentifierService->generatePkiDirectoryName($caId);
|
||||||
$dataDir = $this->config->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data/');
|
$dataDir = $this->config->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data/');
|
||||||
$instanceId = $this->config->getSystemValue('instanceid');
|
$systemInstanceId = $this->config->getSystemValue('instanceid');
|
||||||
$pkiPath = $dataDir . '/appdata_' . $instanceId . '/libresign/' . $pkiDirName;
|
$pkiPath = $dataDir . '/appdata_' . $systemInstanceId . '/libresign/' . $pkiDirName;
|
||||||
|
|
||||||
if (!is_dir($pkiPath)) {
|
if (!is_dir($pkiPath)) {
|
||||||
$this->createDirectoryWithCorrectOwnership($pkiPath);
|
$this->createDirectoryWithCorrectOwnership($pkiPath);
|
||||||
|
|
|
||||||
|
|
@ -62,8 +62,8 @@ class Version15000Date20251209000000 extends SimpleMigrationStep {
|
||||||
if (!$tableFile->hasColumn('signature_flow')) {
|
if (!$tableFile->hasColumn('signature_flow')) {
|
||||||
$tableFile->addColumn('signature_flow', Types::SMALLINT, [
|
$tableFile->addColumn('signature_flow', Types::SMALLINT, [
|
||||||
'notnull' => true,
|
'notnull' => true,
|
||||||
'default' => SignatureFlow::NUMERIC_PARALLEL,
|
'default' => SignatureFlow::NUMERIC_NONE,
|
||||||
'comment' => 'Signature flow mode: 1=parallel, 2=ordered_numeric',
|
'comment' => 'Signature flow mode: 0=none (no admin enforcement), 1=parallel, 2=ordered_numeric',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,7 @@ class RequestSignatureService {
|
||||||
public function saveFile(array $data): FileEntity {
|
public function saveFile(array $data): FileEntity {
|
||||||
if (!empty($data['uuid'])) {
|
if (!empty($data['uuid'])) {
|
||||||
$file = $this->fileMapper->getByUuid($data['uuid']);
|
$file = $this->fileMapper->getByUuid($data['uuid']);
|
||||||
|
$this->updateSignatureFlowIfAllowed($file, $data);
|
||||||
return $this->fileStatusService->updateFileStatusIfUpgrade($file, $data['status'] ?? 0);
|
return $this->fileStatusService->updateFileStatusIfUpgrade($file, $data['status'] ?? 0);
|
||||||
}
|
}
|
||||||
$fileId = null;
|
$fileId = null;
|
||||||
|
|
@ -90,6 +91,7 @@ class RequestSignatureService {
|
||||||
if (!is_null($fileId)) {
|
if (!is_null($fileId)) {
|
||||||
try {
|
try {
|
||||||
$file = $this->fileMapper->getByFileId($fileId);
|
$file = $this->fileMapper->getByFileId($fileId);
|
||||||
|
$this->updateSignatureFlowIfAllowed($file, $data);
|
||||||
return $this->fileStatusService->updateFileStatusIfUpgrade($file, $data['status'] ?? 0);
|
return $this->fileStatusService->updateFileStatusIfUpgrade($file, $data['status'] ?? 0);
|
||||||
} catch (\Throwable) {
|
} catch (\Throwable) {
|
||||||
}
|
}
|
||||||
|
|
@ -118,27 +120,45 @@ class RequestSignatureService {
|
||||||
$file->setStatus(FileEntity::STATUS_ABLE_TO_SIGN);
|
$file->setStatus(FileEntity::STATUS_ABLE_TO_SIGN);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($data['signatureFlow']) && is_string($data['signatureFlow'])) {
|
$this->setSignatureFlow($file, $data);
|
||||||
try {
|
|
||||||
$signatureFlow = \OCA\Libresign\Enum\SignatureFlow::from($data['signatureFlow']);
|
|
||||||
$file->setSignatureFlowEnum($signatureFlow);
|
|
||||||
} catch (\ValueError) {
|
|
||||||
$this->setSignatureFlowFromGlobalConfig($file);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$this->setSignatureFlowFromGlobalConfig($file);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->setDocMdpLevelFromGlobalConfig($file);
|
$this->setDocMdpLevelFromGlobalConfig($file);
|
||||||
|
|
||||||
$this->fileMapper->insert($file);
|
$this->fileMapper->insert($file);
|
||||||
return $file;
|
return $file;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function setSignatureFlowFromGlobalConfig(FileEntity $file): void {
|
private function updateSignatureFlowIfAllowed(FileEntity $file, array $data): void {
|
||||||
$globalFlowValue = $this->appConfig->getValueString(Application::APP_ID, 'signature_flow', SignatureFlow::PARALLEL->value);
|
$adminFlow = $this->appConfig->getValueString(Application::APP_ID, 'signature_flow', SignatureFlow::NONE->value);
|
||||||
$globalFlow = SignatureFlow::from($globalFlowValue);
|
$adminForcedConfig = $adminFlow !== SignatureFlow::NONE->value;
|
||||||
$file->setSignatureFlowEnum($globalFlow);
|
|
||||||
|
if ($adminForcedConfig) {
|
||||||
|
$adminFlowEnum = SignatureFlow::from($adminFlow);
|
||||||
|
if ($file->getSignatureFlowEnum() !== $adminFlowEnum) {
|
||||||
|
$file->setSignatureFlowEnum($adminFlowEnum);
|
||||||
|
$this->fileMapper->update($file);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($data['signatureFlow']) && !empty($data['signatureFlow'])) {
|
||||||
|
$newFlow = SignatureFlow::from($data['signatureFlow']);
|
||||||
|
if ($file->getSignatureFlowEnum() !== $newFlow) {
|
||||||
|
$file->setSignatureFlowEnum($newFlow);
|
||||||
|
$this->fileMapper->update($file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setSignatureFlow(FileEntity $file, array $data): void {
|
||||||
|
$adminFlow = $this->appConfig->getValueString(Application::APP_ID, 'signature_flow', SignatureFlow::NONE->value);
|
||||||
|
|
||||||
|
if (isset($data['signatureFlow']) && !empty($data['signatureFlow'])) {
|
||||||
|
$file->setSignatureFlowEnum(SignatureFlow::from($data['signatureFlow']));
|
||||||
|
} elseif ($adminFlow !== SignatureFlow::NONE->value) {
|
||||||
|
$file->setSignatureFlowEnum(SignatureFlow::from($adminFlow));
|
||||||
|
} else {
|
||||||
|
$file->setSignatureFlowEnum(SignatureFlow::NONE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function setDocMdpLevelFromGlobalConfig(FileEntity $file): void {
|
private function setDocMdpLevelFromGlobalConfig(FileEntity $file): void {
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ class Admin implements ISettings {
|
||||||
$this->initialState->provideInitialState('tsa_username', $this->appConfig->getValueString(Application::APP_ID, 'tsa_username', ''));
|
$this->initialState->provideInitialState('tsa_username', $this->appConfig->getValueString(Application::APP_ID, 'tsa_username', ''));
|
||||||
$this->initialState->provideInitialState('tsa_password', $this->appConfig->getValueString(Application::APP_ID, 'tsa_password', self::PASSWORD_PLACEHOLDER));
|
$this->initialState->provideInitialState('tsa_password', $this->appConfig->getValueString(Application::APP_ID, 'tsa_password', self::PASSWORD_PLACEHOLDER));
|
||||||
$this->initialState->provideInitialState('docmdp_config', $this->docMdpConfigService->getConfig());
|
$this->initialState->provideInitialState('docmdp_config', $this->docMdpConfigService->getConfig());
|
||||||
$this->initialState->provideInitialState('signature_flow', $this->appConfig->getValueString(Application::APP_ID, 'signature_flow', ''));
|
$this->initialState->provideInitialState('signature_flow', $this->appConfig->getValueString(Application::APP_ID, 'signature_flow', \OCA\Libresign\Enum\SignatureFlow::NONE->value));
|
||||||
return new TemplateResponse(Application::APP_ID, 'admin_settings');
|
return new TemplateResponse(Application::APP_ID, 'admin_settings');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6679,6 +6679,11 @@
|
||||||
"format": "int64",
|
"format": "int64",
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
"description": "Numeric code of status * 0 - no signers * 1 - signed * 2 - pending"
|
"description": "Numeric code of status * 0 - no signers * 1 - signed * 2 - pending"
|
||||||
|
},
|
||||||
|
"signatureFlow": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"description": "Signature flow mode: 'parallel' or 'ordered_numeric'. If not provided, uses global configuration"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6529,6 +6529,11 @@
|
||||||
"format": "int64",
|
"format": "int64",
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
"description": "Numeric code of status * 0 - no signers * 1 - signed * 2 - pending"
|
"description": "Numeric code of status * 0 - no signers * 1 - signed * 2 - pending"
|
||||||
|
},
|
||||||
|
"signatureFlow": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"description": "Signature flow mode: 'parallel' or 'ordered_numeric'. If not provided, uses global configuration"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,20 @@
|
||||||
@click="addSigner">
|
@click="addSigner">
|
||||||
{{ t('libresign', 'Add signer') }}
|
{{ t('libresign', 'Add signer') }}
|
||||||
</NcButton>
|
</NcButton>
|
||||||
|
<NcCheckboxRadioSwitch v-if="showPreserveOrder"
|
||||||
|
v-model="preserveOrder"
|
||||||
|
type="switch"
|
||||||
|
@update:checked="onPreserveOrderChange">
|
||||||
|
{{ t('libresign', 'Preserve signing order') }}
|
||||||
|
</NcCheckboxRadioSwitch>
|
||||||
|
<NcButton v-if="showViewOrderButton"
|
||||||
|
type="tertiary"
|
||||||
|
@click="showOrderDiagram = true">
|
||||||
|
<template #icon>
|
||||||
|
<ChartGantt :size="20" />
|
||||||
|
</template>
|
||||||
|
{{ t('libresign', 'View signing order') }}
|
||||||
|
</NcButton>
|
||||||
<Signers event="libresign:edit-signer"
|
<Signers event="libresign:edit-signer"
|
||||||
@signing-order-changed="debouncedSave">
|
@signing-order-changed="debouncedSave">
|
||||||
<template #actions="{signer, closeActions}">
|
<template #actions="{signer, closeActions}">
|
||||||
|
|
@ -192,6 +206,18 @@
|
||||||
</NcButton>
|
</NcButton>
|
||||||
</template>
|
</template>
|
||||||
</NcDialog>
|
</NcDialog>
|
||||||
|
<NcDialog v-if="showOrderDiagram"
|
||||||
|
:name="t('libresign', 'Signing order diagram')"
|
||||||
|
size="large"
|
||||||
|
@closing="showOrderDiagram = false">
|
||||||
|
<SigningOrderDiagram :signers="filesStore.getFile()?.signers || []"
|
||||||
|
:sender-name="currentUserDisplayName" />
|
||||||
|
<template #actions>
|
||||||
|
<NcButton @click="showOrderDiagram = false">
|
||||||
|
{{ t('libresign', 'Close') }}
|
||||||
|
</NcButton>
|
||||||
|
</template>
|
||||||
|
</NcDialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
|
|
@ -200,11 +226,13 @@ import debounce from 'debounce'
|
||||||
|
|
||||||
import svgAccount from '@mdi/svg/svg/account.svg?raw'
|
import svgAccount from '@mdi/svg/svg/account.svg?raw'
|
||||||
import svgEmail from '@mdi/svg/svg/email.svg?raw'
|
import svgEmail from '@mdi/svg/svg/email.svg?raw'
|
||||||
|
import svgInfo from '@mdi/svg/svg/information-outline.svg?raw'
|
||||||
import svgSms from '@mdi/svg/svg/message-processing.svg?raw'
|
import svgSms from '@mdi/svg/svg/message-processing.svg?raw'
|
||||||
import svgWhatsapp from '@mdi/svg/svg/whatsapp.svg?raw'
|
import svgWhatsapp from '@mdi/svg/svg/whatsapp.svg?raw'
|
||||||
import svgXmpp from '@mdi/svg/svg/xmpp.svg?raw'
|
import svgXmpp from '@mdi/svg/svg/xmpp.svg?raw'
|
||||||
|
|
||||||
import Bell from 'vue-material-design-icons/Bell.vue'
|
import Bell from 'vue-material-design-icons/Bell.vue'
|
||||||
|
import ChartGantt from 'vue-material-design-icons/ChartGantt.vue'
|
||||||
import Delete from 'vue-material-design-icons/Delete.vue'
|
import Delete from 'vue-material-design-icons/Delete.vue'
|
||||||
import Draw from 'vue-material-design-icons/Draw.vue'
|
import Draw from 'vue-material-design-icons/Draw.vue'
|
||||||
import FileDocument from 'vue-material-design-icons/FileDocument.vue'
|
import FileDocument from 'vue-material-design-icons/FileDocument.vue'
|
||||||
|
|
@ -227,6 +255,7 @@ import NcActions from '@nextcloud/vue/components/NcActions'
|
||||||
import NcAppSidebar from '@nextcloud/vue/components/NcAppSidebar'
|
import NcAppSidebar from '@nextcloud/vue/components/NcAppSidebar'
|
||||||
import NcAppSidebarTab from '@nextcloud/vue/components/NcAppSidebarTab'
|
import NcAppSidebarTab from '@nextcloud/vue/components/NcAppSidebarTab'
|
||||||
import NcButton from '@nextcloud/vue/components/NcButton'
|
import NcButton from '@nextcloud/vue/components/NcButton'
|
||||||
|
import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
|
||||||
import NcDialog from '@nextcloud/vue/components/NcDialog'
|
import NcDialog from '@nextcloud/vue/components/NcDialog'
|
||||||
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
|
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
|
||||||
import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
|
import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
|
||||||
|
|
@ -234,8 +263,9 @@ import NcModal from '@nextcloud/vue/components/NcModal'
|
||||||
import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
|
import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
|
||||||
|
|
||||||
import IdentifySigner from '../Request/IdentifySigner.vue'
|
import IdentifySigner from '../Request/IdentifySigner.vue'
|
||||||
import VisibleElements from '../Request/VisibleElements.vue'
|
|
||||||
import Signers from '../Signers/Signers.vue'
|
import Signers from '../Signers/Signers.vue'
|
||||||
|
import SigningOrderDiagram from '../SigningOrder/SigningOrderDiagram.vue'
|
||||||
|
import VisibleElements from '../Request/VisibleElements.vue'
|
||||||
|
|
||||||
import svgSignal from '../../../img/logo-signal-app.svg?raw'
|
import svgSignal from '../../../img/logo-signal-app.svg?raw'
|
||||||
import svgTelegram from '../../../img/logo-telegram-app.svg?raw'
|
import svgTelegram from '../../../img/logo-telegram-app.svg?raw'
|
||||||
|
|
@ -262,28 +292,31 @@ export default {
|
||||||
name: 'RequestSignatureTab',
|
name: 'RequestSignatureTab',
|
||||||
mixins: [signingOrderMixin],
|
mixins: [signingOrderMixin],
|
||||||
components: {
|
components: {
|
||||||
|
Bell,
|
||||||
|
ChartGantt,
|
||||||
|
Delete,
|
||||||
|
Draw,
|
||||||
|
FileDocument,
|
||||||
|
IdentifySigner,
|
||||||
|
Information,
|
||||||
|
MessageText,
|
||||||
NcActionButton,
|
NcActionButton,
|
||||||
NcActionInput,
|
NcActionInput,
|
||||||
NcActions,
|
NcActions,
|
||||||
NcAppSidebar,
|
NcAppSidebar,
|
||||||
NcAppSidebarTab,
|
NcAppSidebarTab,
|
||||||
NcButton,
|
NcButton,
|
||||||
|
NcCheckboxRadioSwitch,
|
||||||
|
NcDialog,
|
||||||
NcIconSvgWrapper,
|
NcIconSvgWrapper,
|
||||||
NcLoadingIcon,
|
NcLoadingIcon,
|
||||||
NcModal,
|
NcModal,
|
||||||
NcNoteCard,
|
NcNoteCard,
|
||||||
NcDialog,
|
|
||||||
Bell,
|
|
||||||
Delete,
|
|
||||||
Draw,
|
|
||||||
FileDocument,
|
|
||||||
Information,
|
|
||||||
MessageText,
|
|
||||||
OrderNumericAscending,
|
OrderNumericAscending,
|
||||||
Pencil,
|
Pencil,
|
||||||
Send,
|
Send,
|
||||||
Signers,
|
Signers,
|
||||||
IdentifySigner,
|
SigningOrderDiagram,
|
||||||
VisibleElements,
|
VisibleElements,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
|
@ -311,23 +344,61 @@ export default {
|
||||||
showConfirmRequestSigner: false,
|
showConfirmRequestSigner: false,
|
||||||
selectedSigner: null,
|
selectedSigner: null,
|
||||||
activeTab: '',
|
activeTab: '',
|
||||||
|
preserveOrder: false,
|
||||||
|
showOrderDiagram: false,
|
||||||
|
infoIcon: svgInfo,
|
||||||
|
adminSignatureFlow: '',
|
||||||
|
lastSyncedFileId: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
signatureFlow() {
|
signatureFlow() {
|
||||||
const file = this.filesStore.getFile()
|
const file = this.filesStore.getFile()
|
||||||
return file?.signatureFlow ?? 'parallel'
|
let flow = file?.signatureFlow
|
||||||
|
|
||||||
|
if (typeof flow === 'number') {
|
||||||
|
const flowMap = { 0: 'none', 1: 'parallel', 2: 'ordered_numeric' }
|
||||||
|
flow = flowMap[flow]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flow && flow !== 'none') {
|
||||||
|
return flow
|
||||||
|
}
|
||||||
|
if (this.adminSignatureFlow && this.adminSignatureFlow !== 'none') {
|
||||||
|
return this.adminSignatureFlow
|
||||||
|
}
|
||||||
|
return 'parallel'
|
||||||
|
},
|
||||||
|
isAdminFlowForced() {
|
||||||
|
return this.adminSignatureFlow && this.adminSignatureFlow !== 'none'
|
||||||
},
|
},
|
||||||
isOrderedNumeric() {
|
isOrderedNumeric() {
|
||||||
return this.signatureFlow === 'ordered_numeric'
|
return this.signatureFlow === 'ordered_numeric'
|
||||||
},
|
},
|
||||||
|
showSigningOrderOptions() {
|
||||||
|
return this.hasSigners && this.filesStore.canSave() && !this.isAdminFlowForced
|
||||||
|
},
|
||||||
|
showPreserveOrder() {
|
||||||
|
return this.totalSigners > 1 && this.filesStore.canSave() && !this.isAdminFlowForced
|
||||||
|
},
|
||||||
|
showViewOrderButton() {
|
||||||
|
return this.isOrderedNumeric && this.totalSigners > 1 && this.hasSigners
|
||||||
|
},
|
||||||
|
shouldShowOrderedOptions() {
|
||||||
|
return this.isOrderedNumeric && this.totalSigners > 1
|
||||||
|
},
|
||||||
|
currentUserDisplayName() {
|
||||||
|
return OC.getCurrentUser()?.displayName || ''
|
||||||
|
},
|
||||||
showDocMdpWarning() {
|
showDocMdpWarning() {
|
||||||
return this.filesStore.isDocMdpNoChangesAllowed() && !this.filesStore.canAddSigner()
|
return this.filesStore.isDocMdpNoChangesAllowed() && !this.filesStore.canAddSigner()
|
||||||
},
|
},
|
||||||
canEditSigningOrder() {
|
canEditSigningOrder() {
|
||||||
return (signer) => {
|
return (signer) => {
|
||||||
|
const minSigners = this.isAdminFlowForced ? 1 : 2
|
||||||
|
|
||||||
return this.isOrderedNumeric
|
return this.isOrderedNumeric
|
||||||
&& this.totalSigners > 1
|
&& this.totalSigners >= minSigners
|
||||||
&& this.filesStore.canSave()
|
&& this.filesStore.canSave()
|
||||||
&& !signer.signed
|
&& !signer.signed
|
||||||
}
|
}
|
||||||
|
|
@ -490,12 +561,25 @@ export default {
|
||||||
signers(signers) {
|
signers(signers) {
|
||||||
this.init(signers)
|
this.init(signers)
|
||||||
},
|
},
|
||||||
|
'filesStore.selectedNodeId': {
|
||||||
|
handler(newFileId, oldFileId) {
|
||||||
|
if (newFileId && newFileId !== this.lastSyncedFileId) {
|
||||||
|
this.syncPreserveOrderWithFile()
|
||||||
|
this.lastSyncedFileId = newFileId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
immediate: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
subscribe('libresign:edit-signer', this.editSigner)
|
subscribe('libresign:edit-signer', this.editSigner)
|
||||||
this.filesStore.disableIdentifySigner()
|
this.filesStore.disableIdentifySigner()
|
||||||
|
|
||||||
this.activeTab = this.userConfigStore.signer_identify_tab || ''
|
this.activeTab = this.userConfigStore.signer_identify_tab || ''
|
||||||
|
|
||||||
|
this.adminSignatureFlow = loadState('libresign', 'signature_flow', 'none')
|
||||||
|
|
||||||
|
this.syncPreserveOrderWithFile()
|
||||||
},
|
},
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
unsubscribe('libresign:edit-signer')
|
unsubscribe('libresign:edit-signer')
|
||||||
|
|
@ -506,7 +590,11 @@ export default {
|
||||||
|
|
||||||
this.debouncedSave = debounce(async () => {
|
this.debouncedSave = debounce(async () => {
|
||||||
try {
|
try {
|
||||||
await this.filesStore.saveWithVisibleElements({ visibleElements: [] })
|
const file = this.filesStore.getFile()
|
||||||
|
await this.filesStore.saveWithVisibleElements({
|
||||||
|
visibleElements: [],
|
||||||
|
signatureFlow: file?.signatureFlow,
|
||||||
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.response?.data?.ocs?.data?.message) {
|
if (error.response?.data?.ocs?.data?.message) {
|
||||||
showError(error.response.data.ocs.data.message)
|
showError(error.response.data.ocs.data.message)
|
||||||
|
|
@ -521,6 +609,56 @@ export default {
|
||||||
}, 500)
|
}, 500)
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
onPreserveOrderChange(value) {
|
||||||
|
this.preserveOrder = value
|
||||||
|
const file = this.filesStore.getFile()
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
if (file?.signers) {
|
||||||
|
file.signers.forEach((signer, index) => {
|
||||||
|
if (!signer.signingOrder) {
|
||||||
|
this.$set(signer, 'signingOrder', index + 1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (file) {
|
||||||
|
this.$set(file, 'signatureFlow', 'ordered_numeric')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!this.isAdminFlowForced) {
|
||||||
|
if (file?.signers) {
|
||||||
|
file.signers.forEach(signer => {
|
||||||
|
if (!signer.signed) {
|
||||||
|
this.$set(signer, 'signingOrder', 1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (file) {
|
||||||
|
this.$set(file, 'signatureFlow', 'parallel')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.debouncedSave()
|
||||||
|
},
|
||||||
|
|
||||||
|
syncPreserveOrderWithFile() {
|
||||||
|
const file = this.filesStore.getFile()
|
||||||
|
if (!file) {
|
||||||
|
this.preserveOrder = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const flow = file.signatureFlow
|
||||||
|
|
||||||
|
this.lastSyncedFileId = this.filesStore.selectedNodeId
|
||||||
|
|
||||||
|
if ((flow === 'ordered_numeric' || flow === 2) && !this.isAdminFlowForced) {
|
||||||
|
this.preserveOrder = true
|
||||||
|
} else {
|
||||||
|
this.preserveOrder = false
|
||||||
|
}
|
||||||
|
},
|
||||||
getSvgIcon(name) {
|
getSvgIcon(name) {
|
||||||
return iconMap[`svg${name.charAt(0).toUpperCase() + name.slice(1)}`] || iconMap.svgAccount
|
return iconMap[`svg${name.charAt(0).toUpperCase() + name.slice(1)}`] || iconMap.svgAccount
|
||||||
},
|
},
|
||||||
|
|
@ -789,6 +927,10 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
:deep(.checkbox-radio-switch) {
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
.action-buttons {
|
.action-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,14 @@ export default {
|
||||||
computed: {
|
computed: {
|
||||||
signatureFlow() {
|
signatureFlow() {
|
||||||
const file = this.filesStore.getFile()
|
const file = this.filesStore.getFile()
|
||||||
return file?.signatureFlow ?? 'parallel'
|
let flow = file?.signatureFlow ?? 'parallel'
|
||||||
|
|
||||||
|
if (typeof flow === 'number') {
|
||||||
|
const flowMap = { 0: 'none', 1: 'parallel', 2: 'ordered_numeric' }
|
||||||
|
flow = flowMap[flow] || 'parallel'
|
||||||
|
}
|
||||||
|
|
||||||
|
return flow
|
||||||
},
|
},
|
||||||
signer() {
|
signer() {
|
||||||
const file = this.filesStore.getFile()
|
const file = this.filesStore.getFile()
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,14 @@ export default {
|
||||||
},
|
},
|
||||||
isOrderedNumeric() {
|
isOrderedNumeric() {
|
||||||
const file = this.filesStore.getFile()
|
const file = this.filesStore.getFile()
|
||||||
return file?.signatureFlow === 'ordered_numeric'
|
let flow = file?.signatureFlow
|
||||||
|
|
||||||
|
if (typeof flow === 'number') {
|
||||||
|
const flowMap = { 0: 'none', 1: 'parallel', 2: 'ordered_numeric' }
|
||||||
|
flow = flowMap[flow]
|
||||||
|
}
|
||||||
|
|
||||||
|
return flow === 'ordered_numeric'
|
||||||
},
|
},
|
||||||
canReorder() {
|
canReorder() {
|
||||||
return this.filesStore.canSave() && this.signers.length > 1
|
return this.filesStore.canSave() && this.signers.length > 1
|
||||||
|
|
|
||||||
473
src/Components/SigningOrder/SigningOrderDiagram.vue
Normal file
473
src/Components/SigningOrder/SigningOrderDiagram.vue
Normal file
|
|
@ -0,0 +1,473 @@
|
||||||
|
<!--
|
||||||
|
- SPDX-FileCopyrightText: 2025 LibreCode coop and LibreCode contributors
|
||||||
|
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<div class="signing-order-diagram">
|
||||||
|
<div class="diagram-content">
|
||||||
|
<div class="stage">
|
||||||
|
<div class="stage-label">{{ t('libresign', 'SENDER') }}</div>
|
||||||
|
<div class="stage-items">
|
||||||
|
<div class="signer-node sender">
|
||||||
|
<div class="signer-content">
|
||||||
|
<NcAvatar :user="senderName" :size="40" />
|
||||||
|
<div class="signer-info">
|
||||||
|
<div class="signer-name">{{ senderName }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-for="order in uniqueOrders" :key="order" class="stage">
|
||||||
|
<div class="stage-number">{{ order }}</div>
|
||||||
|
<div class="stage-items">
|
||||||
|
<div v-for="(signer, index) in getSignersByOrder(order)"
|
||||||
|
:key="`${order}-${index}`"
|
||||||
|
class="signer-node"
|
||||||
|
:class="{ signed: signer.signed }">
|
||||||
|
<NcPopover>
|
||||||
|
<template #trigger="{ attrs }">
|
||||||
|
<button class="signer-content"
|
||||||
|
v-bind="attrs"
|
||||||
|
type="button">
|
||||||
|
<div class="avatar-container">
|
||||||
|
<NcAvatar :display-name="getSignerDisplayName(signer)" :size="40" />
|
||||||
|
<div class="status-indicator" :class="getStatusClass(signer)" />
|
||||||
|
</div>
|
||||||
|
<div class="signer-info">
|
||||||
|
<div class="signer-name">{{ getSignerDisplayName(signer) }}</div>
|
||||||
|
<div class="signer-identify">{{ getSignerIdentify(signer) }}</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<div class="popover-content" tabindex="0">
|
||||||
|
<div class="popover-row">
|
||||||
|
<strong>{{ t('libresign', 'Name') }}:</strong>
|
||||||
|
<span>{{ getSignerDisplayName(signer) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="popover-row">
|
||||||
|
<strong>{{ t('libresign', 'Method') }}:</strong>
|
||||||
|
<div class="method-chips">
|
||||||
|
<NcChip v-for="method in getIdentifyMethods(signer)"
|
||||||
|
:key="method"
|
||||||
|
:text="method"
|
||||||
|
:no-close="true" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="popover-row">
|
||||||
|
<strong>{{ t('libresign', 'Contact') }}:</strong>
|
||||||
|
<span>{{ getSignerIdentify(signer) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="popover-row">
|
||||||
|
<strong>{{ t('libresign', 'Status') }}:</strong>
|
||||||
|
<NcChip :text="getStatusLabel(signer)"
|
||||||
|
:type="getChipType(signer)"
|
||||||
|
:icon-path="getStatusIconPath(signer)"
|
||||||
|
:no-close="true" />
|
||||||
|
</div>
|
||||||
|
<div class="popover-row">
|
||||||
|
<strong>{{ t('libresign', 'Order') }}:</strong>
|
||||||
|
<span>{{ signer.signingOrder || 1 }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="signer.signed && signer.signDate" class="popover-row">
|
||||||
|
<strong>{{ t('libresign', 'Signed at') }}:</strong>
|
||||||
|
<span>{{ formatDate(signer.signDate) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</NcPopover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stage">
|
||||||
|
<div class="stage-label">{{ t('libresign', 'COMPLETED') }}</div>
|
||||||
|
<div class="stage-items">
|
||||||
|
<div class="signer-node completed">
|
||||||
|
<div class="signer-icon">
|
||||||
|
<Check :size="24" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mdiCheckCircle, mdiClockOutline, mdiCircleOutline } from '@mdi/js'
|
||||||
|
import Check from 'vue-material-design-icons/Check.vue'
|
||||||
|
|
||||||
|
import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js'
|
||||||
|
import NcChip from '@nextcloud/vue/dist/Components/NcChip.js'
|
||||||
|
import NcPopover from '@nextcloud/vue/dist/Components/NcPopover.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SigningOrderDiagram',
|
||||||
|
components: {
|
||||||
|
NcAvatar,
|
||||||
|
NcChip,
|
||||||
|
NcPopover,
|
||||||
|
Check,
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
mdiCheckCircle,
|
||||||
|
mdiClockOutline,
|
||||||
|
mdiCircleOutline,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
signers: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
senderName: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
uniqueOrders() {
|
||||||
|
const orders = this.signers.map(s => s.signingOrder || 1)
|
||||||
|
return [...new Set(orders)].sort((a, b) => a - b)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getSignersByOrder(order) {
|
||||||
|
return this.signers.filter(s => (s.signingOrder || 1) === order)
|
||||||
|
},
|
||||||
|
getSignerDisplayName(signer) {
|
||||||
|
return signer.displayName || signer.identifyMethods?.[0]?.value || this.t('libresign', 'Signer')
|
||||||
|
},
|
||||||
|
getSignerIdentify(signer) {
|
||||||
|
const method = signer.identifyMethods?.[0]?.method
|
||||||
|
const value = signer.identifyMethods?.[0]?.value
|
||||||
|
if (method === 'email') {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
if (method === 'account') {
|
||||||
|
return `v.${method}.${value}@colab.rio`
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
},
|
||||||
|
getIdentifyMethods(signer) {
|
||||||
|
return signer.identifyMethods?.map(method => method.method) || []
|
||||||
|
},
|
||||||
|
getStatusLabel(signer) {
|
||||||
|
if (signer.signed) return this.t('libresign', 'Signed')
|
||||||
|
if (signer.me?.status === 0) return this.t('libresign', 'Draft')
|
||||||
|
return this.t('libresign', 'Pending')
|
||||||
|
},
|
||||||
|
getStatusIconPath(signer) {
|
||||||
|
if (signer.signed) return this.mdiCheckCircle
|
||||||
|
if (signer.me?.status === 0) return this.mdiCircleOutline
|
||||||
|
return this.mdiClockOutline
|
||||||
|
},
|
||||||
|
getChipType(signer) {
|
||||||
|
if (signer.signed) return 'success'
|
||||||
|
if (signer.me?.status === 0) return 'secondary'
|
||||||
|
return 'warning'
|
||||||
|
},
|
||||||
|
getStatusClass(signer) {
|
||||||
|
if (signer.signed) return 'signed'
|
||||||
|
if (signer.me?.status === 0) return 'draft'
|
||||||
|
return 'pending'
|
||||||
|
},
|
||||||
|
formatDate(timestamp) {
|
||||||
|
if (!timestamp) return ''
|
||||||
|
const date = new Date(timestamp * 1000)
|
||||||
|
return date.toLocaleString()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.signing-order-diagram {
|
||||||
|
padding: 20px 16px;
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
@media (max-width: 512px) {
|
||||||
|
padding: 16px 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.diagram-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
max-width: 700px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
|
||||||
|
@media (max-width: 512px) {
|
||||||
|
max-width: 100%;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
padding: 16px 0;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:last-child)::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: -10px;
|
||||||
|
left: 50%;
|
||||||
|
width: 2px;
|
||||||
|
height: 10px;
|
||||||
|
background: var(--color-border-dark);
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
@media (max-width: 512px) {
|
||||||
|
bottom: -8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-maxcontrast);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
@media (max-width: 512px) {
|
||||||
|
font-size: 11px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-number {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 16px;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-maxcontrast);
|
||||||
|
width: 50px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
@media (max-width: 512px) {
|
||||||
|
font-size: 20px;
|
||||||
|
width: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-items {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
@media (max-width: 512px) {
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.signer-node {
|
||||||
|
background: var(--color-main-background);
|
||||||
|
border: 2px solid var(--color-border);
|
||||||
|
border-radius: 50px;
|
||||||
|
min-width: 220px;
|
||||||
|
max-width: 100%;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
@media (max-width: 512px) {
|
||||||
|
min-width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.signer-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 20px;
|
||||||
|
width: 100%;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
@media (max-width: 512px) {
|
||||||
|
padding: 10px 16px;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.sender {
|
||||||
|
background: var(--color-primary-element-light);
|
||||||
|
border-color: var(--color-primary-element);
|
||||||
|
|
||||||
|
.signer-name {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.completed {
|
||||||
|
background: var(--color-success);
|
||||||
|
border-color: var(--color-success);
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 64px;
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
@media (max-width: 512px) {
|
||||||
|
min-width: 56px;
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signer-icon {
|
||||||
|
background: transparent;
|
||||||
|
color: white;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
|
||||||
|
@media (max-width: 512px) {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-container {
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
.status-indicator {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border: 2px solid var(--color-main-background);
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
@media (max-width: 512px) {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.signed {
|
||||||
|
background: var(--color-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.pending {
|
||||||
|
background: var(--color-warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.draft {
|
||||||
|
background: var(--color-text-maxcontrast);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.signer-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
@media (max-width: 512px) {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.signer-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
.signer-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-main-text);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
@media (max-width: 512px) {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.signer-identify {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-text-maxcontrast);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
margin-top: 2px;
|
||||||
|
|
||||||
|
@media (max-width: 512px) {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.popover-content {
|
||||||
|
padding: 12px;
|
||||||
|
min-width: 250px;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
.popover-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 80px 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
align-items: start;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
strong {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--color-text-maxcontrast);
|
||||||
|
padding-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--color-main-text);
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-chips {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -381,8 +381,15 @@ export const useFilesStore = function(...args) {
|
||||||
filesSorted() {
|
filesSorted() {
|
||||||
return this.ordered.map(key => this.files[key])
|
return this.ordered.map(key => this.files[key])
|
||||||
},
|
},
|
||||||
async saveWithVisibleElements({ visibleElements = [], signers = null, uuid = null, nodeId = null }) {
|
async saveWithVisibleElements({ visibleElements = [], signers = null, uuid = null, nodeId = null, signatureFlow = null }) {
|
||||||
const file = this.getFile()
|
const file = this.getFile()
|
||||||
|
|
||||||
|
let flowValue = signatureFlow || file.signatureFlow
|
||||||
|
if (typeof flowValue === 'number') {
|
||||||
|
const flowMap = { 0: 'none', 1: 'parallel', 2: 'ordered_numeric' }
|
||||||
|
flowValue = flowMap[flowValue] || 'parallel'
|
||||||
|
}
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
url: generateOcsUrl('/apps/libresign/api/v1/request-signature'),
|
url: generateOcsUrl('/apps/libresign/api/v1/request-signature'),
|
||||||
method: uuid || file.uuid ? 'patch' : 'post',
|
method: uuid || file.uuid ? 'patch' : 'post',
|
||||||
|
|
@ -391,10 +398,12 @@ export const useFilesStore = function(...args) {
|
||||||
users: signers || file.signers,
|
users: signers || file.signers,
|
||||||
visibleElements,
|
visibleElements,
|
||||||
status: 0,
|
status: 0,
|
||||||
|
signatureFlow: flowValue,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if (uuid || file.uuid) {
|
|
||||||
|
if (uuid || file.uuid) {
|
||||||
config.data.uuid = uuid || file.uuid
|
config.data.uuid = uuid || file.uuid
|
||||||
} else {
|
} else {
|
||||||
config.data.file = {
|
config.data.file = {
|
||||||
|
|
@ -406,8 +415,15 @@ export const useFilesStore = function(...args) {
|
||||||
this.addFile(data.ocs.data.data)
|
this.addFile(data.ocs.data.data)
|
||||||
return data.ocs.data
|
return data.ocs.data
|
||||||
},
|
},
|
||||||
async updateSignatureRequest({ visibleElements = [], signers = null, uuid = null, nodeId = null, status = 1 }) {
|
async updateSignatureRequest({ visibleElements = [], signers = null, uuid = null, nodeId = null, status = 1, signatureFlow = null }) {
|
||||||
const file = this.getFile()
|
const file = this.getFile()
|
||||||
|
|
||||||
|
let flowValue = signatureFlow || file.signatureFlow
|
||||||
|
if (typeof flowValue === 'number') {
|
||||||
|
const flowMap = { 0: 'none', 1: 'parallel', 2: 'ordered_numeric' }
|
||||||
|
flowValue = flowMap[flowValue] || 'parallel'
|
||||||
|
}
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
url: generateOcsUrl('/apps/libresign/api/v1/request-signature'),
|
url: generateOcsUrl('/apps/libresign/api/v1/request-signature'),
|
||||||
method: uuid || file.uuid ? 'patch' : 'post',
|
method: uuid || file.uuid ? 'patch' : 'post',
|
||||||
|
|
@ -416,6 +432,7 @@ export const useFilesStore = function(...args) {
|
||||||
users: signers || file.signers,
|
users: signers || file.signers,
|
||||||
visibleElements,
|
visibleElements,
|
||||||
status,
|
status,
|
||||||
|
signatureFlow: flowValue,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -426,7 +443,6 @@ export const useFilesStore = function(...args) {
|
||||||
fileId: nodeId || this.selectedNodeId,
|
fileId: nodeId || this.selectedNodeId,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data } = await axios(config)
|
const { data } = await axios(config)
|
||||||
this.addFile(data.ocs.data.data)
|
this.addFile(data.ocs.data.data)
|
||||||
return data.ocs.data
|
return data.ocs.data
|
||||||
|
|
|
||||||
|
|
@ -4106,6 +4106,8 @@ export interface operations {
|
||||||
* @description Numeric code of status * 0 - no signers * 1 - signed * 2 - pending
|
* @description Numeric code of status * 0 - no signers * 1 - signed * 2 - pending
|
||||||
*/
|
*/
|
||||||
status?: number | null;
|
status?: number | null;
|
||||||
|
/** @description Signature flow mode: 'parallel' or 'ordered_numeric'. If not provided, uses global configuration */
|
||||||
|
signatureFlow?: string | null;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -3628,6 +3628,8 @@ export interface operations {
|
||||||
* @description Numeric code of status * 0 - no signers * 1 - signed * 2 - pending
|
* @description Numeric code of status * 0 - no signers * 1 - signed * 2 - pending
|
||||||
*/
|
*/
|
||||||
status?: number | null;
|
status?: number | null;
|
||||||
|
/** @description Signature flow mode: 'parallel' or 'ordered_numeric'. If not provided, uses global configuration */
|
||||||
|
signatureFlow?: string | null;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -104,9 +104,9 @@ export default {
|
||||||
methods: {
|
methods: {
|
||||||
loadConfig() {
|
loadConfig() {
|
||||||
try {
|
try {
|
||||||
const mode = loadState('libresign', 'signature_flow', null)
|
const mode = loadState('libresign', 'signature_flow', 'none')
|
||||||
|
|
||||||
if (mode === null || mode === '') {
|
if (mode === 'none') {
|
||||||
this.enabled = false
|
this.enabled = false
|
||||||
this.selectedFlow = this.availableFlows[0]
|
this.selectedFlow = this.availableFlows[0]
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
final class SignatureFlowTest extends TestCase {
|
final class SignatureFlowTest extends TestCase {
|
||||||
public static function validFlowProvider(): array {
|
public static function validFlowProvider(): array {
|
||||||
return [
|
return [
|
||||||
|
'none' => [SignatureFlow::NONE, SignatureFlow::NUMERIC_NONE, 'none'],
|
||||||
'parallel' => [SignatureFlow::PARALLEL, SignatureFlow::NUMERIC_PARALLEL, 'parallel'],
|
'parallel' => [SignatureFlow::PARALLEL, SignatureFlow::NUMERIC_PARALLEL, 'parallel'],
|
||||||
'ordered_numeric' => [SignatureFlow::ORDERED_NUMERIC, SignatureFlow::NUMERIC_ORDERED_NUMERIC, 'ordered_numeric'],
|
'ordered_numeric' => [SignatureFlow::ORDERED_NUMERIC, SignatureFlow::NUMERIC_ORDERED_NUMERIC, 'ordered_numeric'],
|
||||||
];
|
];
|
||||||
|
|
@ -29,7 +30,6 @@ final class SignatureFlowTest extends TestCase {
|
||||||
|
|
||||||
public static function invalidNumericProvider(): array {
|
public static function invalidNumericProvider(): array {
|
||||||
return [
|
return [
|
||||||
'zero' => [0],
|
|
||||||
'negative' => [-1],
|
'negative' => [-1],
|
||||||
'three' => [3],
|
'three' => [3],
|
||||||
'large' => [999],
|
'large' => [999],
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue