mirror of
https://github.com/LibreSign/libresign.git
synced 2025-12-18 05:20:45 +01:00
Display PDF document with lib (#1996)
Signed-off-by: Vitor Mattos <vitor@php.rio>
This commit is contained in:
parent
8dc227c26a
commit
5902e9c4a6
12 changed files with 1369 additions and 591 deletions
|
|
@ -204,7 +204,7 @@ class PageController extends AEnvironmentPageAwareController {
|
|||
#[NoCSRFRequired]
|
||||
#[RequireSetupOk]
|
||||
#[PublicPage]
|
||||
#[AnonRateLimit(limit: 5, period: 120)]
|
||||
#[AnonRateLimit(limit: 12, period: 60)]
|
||||
public function getPdf($uuid) {
|
||||
$this->throwIfValidationPageNotAccessible();
|
||||
try {
|
||||
|
|
@ -229,7 +229,7 @@ class PageController extends AEnvironmentPageAwareController {
|
|||
#[RequireSignRequestUuid]
|
||||
#[PublicPage]
|
||||
#[RequireSetupOk]
|
||||
#[AnonRateLimit(limit: 5, period: 120)]
|
||||
#[AnonRateLimit(limit: 12, period: 60)]
|
||||
public function getPdfUser($uuid) {
|
||||
$this->throwIfValidationPageNotAccessible();
|
||||
$resp = new FileDisplayResponse($this->getNextcloudFile());
|
||||
|
|
|
|||
|
|
@ -241,7 +241,7 @@ class ValidateHelper {
|
|||
throw new LibresignException($this->l10n->t('Coordinate %s must be an integer', [$type]));
|
||||
}
|
||||
if ($value < 0) {
|
||||
throw new LibresignException($this->l10n->t('Coordinate %s must be equal to or greater than 0', [$type]));
|
||||
throw new LibresignException($this->l10n->t('Out of marging'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ class FileElementService {
|
|||
$dimension = $metadata['d'][$properties['coordinates']['page'] - 1];
|
||||
|
||||
$translated['left'] = $properties['coordinates']['llx'];
|
||||
$translated['height'] = $properties['coordinates']['ury'] - $properties['coordinates']['lly'];
|
||||
$translated['height'] = abs($properties['coordinates']['ury'] - $properties['coordinates']['lly']);
|
||||
$translated['top'] = $dimension['h'] - $properties['coordinates']['ury'];
|
||||
$translated['width'] = $properties['coordinates']['urx'] - $properties['coordinates']['llx'];
|
||||
|
||||
|
|
@ -134,7 +134,7 @@ class FileElementService {
|
|||
|
||||
public function deleteVisibleElement(int $elementId): void {
|
||||
$fileElement = new FileElement();
|
||||
$fileElement->fromRow(['id' => $elementId]);
|
||||
$fileElement = $fileElement->fromRow(['id' => $elementId]);
|
||||
$this->fileElementMapper->delete($fileElement);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -270,9 +270,6 @@ class FileService {
|
|||
private function getVisibleElements(): array {
|
||||
$return = [];
|
||||
try {
|
||||
if ($this->me) {
|
||||
$uid = $this->me->getUID();
|
||||
}
|
||||
if (is_object($this->signRequest)) {
|
||||
$visibleElements = $this->fileElementMapper->getByFileIdAndSignRequestId($this->file->getId(), $this->signRequest->getId());
|
||||
} else {
|
||||
|
|
@ -291,14 +288,6 @@ class FileService {
|
|||
'lly' => $visibleElement->getLly()
|
||||
]
|
||||
];
|
||||
if (!empty($uid) && $uid === $this->file->getUserId()) {
|
||||
$signRequest = $this->signRequestMapper->getById($visibleElement->getSignRequestId());
|
||||
$userAssociatedToVisibleElement = $this->userManager->getByEmail($signRequest->getEmail());
|
||||
if ($userAssociatedToVisibleElement) {
|
||||
$element['uid'] = $userAssociatedToVisibleElement[0]->getUID();
|
||||
}
|
||||
$element['email'] = $signRequest->getEmail();
|
||||
}
|
||||
$element['coordinates'] = array_merge(
|
||||
$element['coordinates'],
|
||||
$this->fileElementService->translateCoordinatesFromInternalNotation($element, $this->file)
|
||||
|
|
|
|||
718
package-lock.json
generated
718
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -8,6 +8,7 @@
|
|||
"build": "webpack --node-env production --progress",
|
||||
"dev": "webpack --node-env development --progress",
|
||||
"watch": "webpack --node-env development --progress --watch",
|
||||
"serve": "webpack serve --node-env development --progress --allowed-hosts all --host 0.0.0.0",
|
||||
"lint": "eslint --ext .js,.vue src",
|
||||
"lint:fix": "eslint --ext .js,.vue src --fix",
|
||||
"stylelint": "stylelint css/*.css css/*.scss src/**/*.scss src/**/*.vue",
|
||||
|
|
@ -18,6 +19,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@fontsource/dancing-script": "^5.0.16",
|
||||
"@libresign/vue-pdf-editor": "^1.2.2",
|
||||
"@marionebl/option": "^1.0.8",
|
||||
"@nextcloud/auth": "^2.1.0",
|
||||
"@nextcloud/axios": "^2.4.0",
|
||||
|
|
|
|||
383
src/Components/Nextcloud/NcDialog/NcDialog.vue
Normal file
383
src/Components/Nextcloud/NcDialog/NcDialog.vue
Normal file
|
|
@ -0,0 +1,383 @@
|
|||
<template>
|
||||
<NcModal v-if="open"
|
||||
v-bind="modalProps"
|
||||
class="modal-container__content"
|
||||
@close="handleClosed"
|
||||
@update:show="handleClosing">
|
||||
<!-- The dialog name / header -->
|
||||
<h2 :id="navigationId" class="dialog__name" v-text="name" />
|
||||
<div class="dialog" :class="dialogClasses">
|
||||
<div :class="['dialog__wrapper', { 'dialog__wrapper--collapsed': isNavigationCollapsed }]">
|
||||
<!-- When the navigation is collapsed (too small dialog) it is displayed above the main content, otherwise on the inline start -->
|
||||
<nav v-if="hasNavigation"
|
||||
class="dialog__navigation"
|
||||
:class="navigationClasses"
|
||||
:aria-labelledby="navigationId">
|
||||
<slot name="navigation" :is-collapsed="isNavigationCollapsed" />
|
||||
</nav>
|
||||
<!-- Main dialog content -->
|
||||
<div class="dialog__content" :class="contentClasses">
|
||||
<slot>
|
||||
<p class="dialog__text">
|
||||
{{ message }}
|
||||
</p>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
<!-- The dialog actions aka the buttons -->
|
||||
<div class="dialog__actions">
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
</div>
|
||||
</NcModal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed, defineComponent, ref } from 'vue'
|
||||
|
||||
import NcModal from '@nextcloud/vue/dist/Components/NcModal.js'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NcDialog',
|
||||
|
||||
components: {
|
||||
NcModal,
|
||||
},
|
||||
|
||||
props: {
|
||||
/** Name of the dialog (the heading) */
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
/** Text of the dialog */
|
||||
message: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
/** Additional elements to add to the focus trap */
|
||||
additionalTrapElements: {
|
||||
type: Array,
|
||||
validator: (arr) => Array.isArray(arr) && arr.every((element) => typeof element === 'string'),
|
||||
default: () => ([]),
|
||||
},
|
||||
|
||||
/**
|
||||
* The element where to mount the dialog, if `null` is passed the dialog is mounted in place
|
||||
* @default 'body'
|
||||
*/
|
||||
container: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'body',
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether the dialog should be shown
|
||||
* @default true
|
||||
*/
|
||||
open: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
|
||||
/**
|
||||
* Size of the underlying NcModal
|
||||
* @default 'small'
|
||||
* @type {'small'|'normal'|'large'|'full'}
|
||||
*/
|
||||
size: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'small',
|
||||
validator: (value) => typeof value === 'string' && ['small', 'normal', 'large', 'full'].includes(value),
|
||||
},
|
||||
|
||||
/**
|
||||
* Buttons to display
|
||||
* @default []
|
||||
*/
|
||||
buttons: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => ([]),
|
||||
validator: (value) => Array.isArray(value) && value.every((element) => typeof element === 'object'),
|
||||
},
|
||||
|
||||
/**
|
||||
* Set to false to no show a close button on the dialog
|
||||
* @default true
|
||||
*/
|
||||
canClose: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
|
||||
/**
|
||||
* Close the dialog if the user clicked outside of the dialog
|
||||
* Only relevant if `canClose` is set to true.
|
||||
*/
|
||||
closeOnClickOutside: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
/**
|
||||
* Declare if hiding the modal should be animated
|
||||
* @default false
|
||||
*/
|
||||
outTransition: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
/**
|
||||
* Optionally pass additionaly classes which will be set on the navigation for custom styling
|
||||
* @default ''
|
||||
* @example
|
||||
* ```html
|
||||
* <DialogBase :navigation-classes="['mydialog-navigation']"><!-- --></DialogBase>
|
||||
* <!-- ... -->
|
||||
* <style lang="scss">
|
||||
* :deep(.mydialog-navigation) {
|
||||
* flex-direction: row-reverse;
|
||||
* }
|
||||
* </style>
|
||||
* ```
|
||||
*/
|
||||
navigationClasses: {
|
||||
type: [String, Array, Object],
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
|
||||
/**
|
||||
* Optionally pass additionaly classes which will be set on the content wrapper for custom styling
|
||||
* @default ''
|
||||
*/
|
||||
contentClasses: {
|
||||
type: [String, Array, Object],
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
|
||||
/**
|
||||
* Optionally pass additionaly classes which will be set on the dialog itself
|
||||
* (the default `class` attribute will be set on the modal wrapper)
|
||||
* @default ''
|
||||
*/
|
||||
dialogClasses: {
|
||||
type: [String, Array, Object],
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
emits: ['closing', 'update:open'],
|
||||
|
||||
setup(props, { emit, slots }) {
|
||||
/**
|
||||
* We use the dialog width to decide if we collapse the navigation (flex direction row)
|
||||
*/
|
||||
const { width: dialogWidth } = { width: 900 }
|
||||
|
||||
/**
|
||||
* Whether the navigation is collapsed due to dialog and window size
|
||||
* (collapses when modal is below: 900px modal width - 2x 12px margin)
|
||||
*/
|
||||
const isNavigationCollapsed = computed(() => dialogWidth.value < 876)
|
||||
|
||||
/**
|
||||
* Whether a navigation was passed and the element should be displayed
|
||||
*/
|
||||
const hasNavigation = computed(() => slots?.navigation !== undefined)
|
||||
|
||||
/**
|
||||
* The unique id of the nav element
|
||||
*/
|
||||
const navigationId = ref(Math.random()
|
||||
.toString(36)
|
||||
.replace(/[^a-z]+/g, '')
|
||||
.slice(0, 5),
|
||||
)
|
||||
|
||||
/**
|
||||
* If the underlaying modal is shown
|
||||
*/
|
||||
const showModal = ref(true)
|
||||
|
||||
// Because NcModal does not emit `close` when show prop is changed
|
||||
/**
|
||||
* Handle clicking a dialog button -> should close
|
||||
*/
|
||||
const handleButtonClose = () => {
|
||||
handleClosing()
|
||||
window.setTimeout(() => handleClosed(), 300)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle closing the dialog, optional out transition did not run yet
|
||||
*/
|
||||
const handleClosing = () => {
|
||||
showModal.value = false
|
||||
/**
|
||||
* Emitted when the dialog is closing, so the out transition did not finish yet
|
||||
*/
|
||||
emit('closing')
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle dialog closed (out transition finished)
|
||||
*/
|
||||
const handleClosed = () => {
|
||||
showModal.value = true
|
||||
/**
|
||||
* Emitted then the dialog is fully closed and the out transition run
|
||||
*/
|
||||
emit('update:open', false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Properties to pass to the underlying NcModal
|
||||
*/
|
||||
const modalProps = computed(() => ({
|
||||
canClose: props.canClose,
|
||||
container: props.container === undefined ? 'body' : props.container,
|
||||
// we do not pass the name as we already have the name as the headline
|
||||
// name: props.name,
|
||||
size: props.size,
|
||||
show: props.open && showModal.value,
|
||||
outTransition: props.outTransition,
|
||||
closeOnClickOutside: props.closeOnClickOutside,
|
||||
enableSlideshow: false,
|
||||
enableSwipe: false,
|
||||
}))
|
||||
|
||||
return {
|
||||
handleButtonClose,
|
||||
handleClosing,
|
||||
handleClosed,
|
||||
hasNavigation,
|
||||
navigationId,
|
||||
isNavigationCollapsed,
|
||||
modalProps,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
/** When having the small dialog style we override the modal styling so dialogs look more dialog like */
|
||||
@media only screen and (max-width: 1024px) {
|
||||
.dialog__modal .modal-wrapper--small .modal-container {
|
||||
width: fit-content;
|
||||
height: unset;
|
||||
max-height: 90%;
|
||||
position: relative;
|
||||
top: unset;
|
||||
border-radius: var(--border-radius-large);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dialog {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
overflow: hidden;
|
||||
padding-block: 4px 8px;
|
||||
padding-inline: 12px 8px;
|
||||
|
||||
&__modal {
|
||||
:deep(.modal-wrapper .modal-container) {
|
||||
display: flex !important;
|
||||
padding-block: 4px 8px; // 4px to align with close button, 8px block-end to allow the actions a margin of 4px for the focus visible outline
|
||||
padding-inline: 12px 8px; // Same as with padding-block, we need the actions to have a margin of 4px for the button outline
|
||||
}
|
||||
:deep(.modal-container__content) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
&__wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
// Auto scale to fit
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
// see modal-container padding, this aligns with the padding-inline-start (8px + 4px = 12px)
|
||||
padding-inline-end: 4px;
|
||||
|
||||
&--collapsed {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
&__navigation {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
// Navigation styling when side-by-side with content
|
||||
&__wrapper:not(&__wrapper--collapsed) &__navigation {
|
||||
flex-direction: column;
|
||||
|
||||
overflow: hidden auto;
|
||||
height: 100%;
|
||||
min-width: 200px;
|
||||
margin-inline-end: 20px;
|
||||
}
|
||||
|
||||
// Navigation styling when on top of content
|
||||
&__wrapper#{&}__wrapper--collapsed &__navigation {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
overflow: auto hidden;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
&__name {
|
||||
// Same as the NcAppSettingsDialog
|
||||
text-align: center;
|
||||
height: var(--default-clickable-area);
|
||||
min-height: var(--default-clickable-area);
|
||||
line-height: var(--default-clickable-area);
|
||||
margin-block-end: 12px;
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
margin-bottom: 12px;
|
||||
color: var(--color-text-light);
|
||||
}
|
||||
|
||||
&__content {
|
||||
// Auto fit
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
// In case only text content is show
|
||||
&__text {
|
||||
// Also add padding to the bottom to make it more readable
|
||||
padding-block-end: 6px;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-content: center;
|
||||
width: fit-content;
|
||||
margin-inline: auto 4px; // 4px to align with the overall modal padding, we need this here for the buttons to have their 4px focus-visible outline
|
||||
margin-block: 6px 4px; // 4px block-end see reason above
|
||||
}
|
||||
}
|
||||
</style>
|
||||
73
src/Components/PdfEditor/PdfEditor.vue
Normal file
73
src/Components/PdfEditor/PdfEditor.vue
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
<template>
|
||||
<VuePdfEditor ref="vuePdfEditor"
|
||||
width="100%"
|
||||
height="100%"
|
||||
:show-choose-file-btn="false"
|
||||
:show-customize-editor="false"
|
||||
:show-line-size-select="false"
|
||||
:show-font-size-select="false"
|
||||
:show-font-select="false"
|
||||
:show-save-btn="false"
|
||||
:save-to-upload="false"
|
||||
:init-file-src="fileSrc"
|
||||
:init-image-scale="1"
|
||||
:seal-image-show="false"
|
||||
@pdf-editor:end-init="endInit">
|
||||
<template #custom="{ object, pagesScale }">
|
||||
<Signature :x="object.x"
|
||||
:y="object.y"
|
||||
:fix-size="false"
|
||||
:display-name="object.signer.displayName"
|
||||
:width="object.width"
|
||||
:height="object.height"
|
||||
:origin-width="object.originWidth"
|
||||
:origin-height="object.originHeight"
|
||||
:page-scale="pagesScale"
|
||||
@onUpdate="$refs.vuePdfEditor.updateObject(object.id, $event)"
|
||||
@onDelete="onDeleteSigner(object)" />
|
||||
</template>
|
||||
</VuePdfEditor>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import VuePdfEditor from '@libresign/vue-pdf-editor'
|
||||
import Signature from './Signature.vue'
|
||||
|
||||
export default {
|
||||
name: 'PdfEditor',
|
||||
components: {
|
||||
VuePdfEditor,
|
||||
Signature,
|
||||
},
|
||||
props: {
|
||||
fileSrc: {
|
||||
type: String,
|
||||
default: '',
|
||||
require: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
endInit(event) {
|
||||
this.$emit('pdf-editor:end-init', { ...event })
|
||||
},
|
||||
onDeleteSigner(object) {
|
||||
this.$emit('pdf-editor:on-delete-signer', object)
|
||||
this.$refs.vuePdfEditor.deleteObject(object.id)
|
||||
},
|
||||
addSigner(signer) {
|
||||
const object = {
|
||||
id: this.$refs.vuePdfEditor.genID(),
|
||||
type: 'custom',
|
||||
signer,
|
||||
width: signer.element.coordinates.width,
|
||||
height: signer.element.coordinates.height,
|
||||
originWidth: signer.element.coordinates.width,
|
||||
originHeight: signer.element.coordinates.height,
|
||||
x: signer.element.coordinates.llx,
|
||||
y: signer.element.coordinates.ury,
|
||||
}
|
||||
this.$refs.vuePdfEditor.addObject(object)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
244
src/Components/PdfEditor/Signature.vue
Normal file
244
src/Components/PdfEditor/Signature.vue
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
<template>
|
||||
<div class="absolute left-0 top-0 select-none"
|
||||
:style="{
|
||||
width: `${width + dw}px`,
|
||||
height: `${Math.round((width + dw) / ratio)}px`,
|
||||
transform: `translate(${x + dx}px, ${y + dy}px)`,
|
||||
}">
|
||||
<div class="absolute w-full h-full cursor-grab"
|
||||
:class="[
|
||||
operation === 'move' ? 'cursor-grabbing' : '',
|
||||
operation ? 'operation' : '',
|
||||
]"
|
||||
@mousedown="handlePanStart"
|
||||
@touchstart="handlePanStart">
|
||||
<div v-if="!fixSize"
|
||||
data-direction="left-top"
|
||||
class="absolute cursor-nwse-resize transform selector"
|
||||
:style="{ top: '0%', left: '0%' }" />
|
||||
<div v-if="!fixSize"
|
||||
data-direction="right-top"
|
||||
class="absolute cursor-nesw-resize transform selector"
|
||||
:style="{ top: '0%', left: '100%' }" />
|
||||
<div v-if="!fixSize"
|
||||
data-direction="left-bottom"
|
||||
class="absolute cursor-nesw-resize transform selector"
|
||||
:style="{ top: '100%', left: '0%' }" />
|
||||
<div v-if="!fixSize"
|
||||
data-direction="right-bottom"
|
||||
class="absolute cursor-nwse-resize transform selector"
|
||||
:style="{ top: '100%', left: '100%' }" />
|
||||
</div>
|
||||
<div class="absolute cursor-pointer transform delete"
|
||||
:style="{ top: '0%', left: '50%' }"
|
||||
@click="onDelete">
|
||||
<CloseCircleIcon class="w-full h-full"
|
||||
text="Remove"
|
||||
fill-color="red"
|
||||
:size="25" />
|
||||
</div>
|
||||
<div class="w-full h-full border border-gray-400 border-dashed content">
|
||||
{{ displayName }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import itemEventsMixin from '@libresign/vue-pdf-editor/src/Components/ItemEventsMixin.vue'
|
||||
import CloseCircleIcon from 'vue-material-design-icons/CloseCircle.vue'
|
||||
|
||||
export default {
|
||||
name: 'Signature',
|
||||
components: {
|
||||
CloseCircleIcon,
|
||||
},
|
||||
mixins: [itemEventsMixin],
|
||||
props: {
|
||||
displayName: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
originWidth: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
originHeight: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
x: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
y: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
pageScale: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
fixSize: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
startX: null,
|
||||
startY: null,
|
||||
operation: '',
|
||||
directions: [],
|
||||
dx: 0,
|
||||
dy: 0,
|
||||
dw: 0,
|
||||
dh: 0,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
ratio() {
|
||||
return this.originWidth / this.originHeight
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
await this.render()
|
||||
},
|
||||
methods: {
|
||||
async render() {
|
||||
let scale = 1
|
||||
const MAX_TARGET = 500
|
||||
if (this.width > MAX_TARGET) {
|
||||
scale = MAX_TARGET / this.width
|
||||
}
|
||||
if (this.height > MAX_TARGET) {
|
||||
scale = Math.min(scale, MAX_TARGET / this.height)
|
||||
}
|
||||
// eslint-disable-next-line vue/custom-event-name-casing
|
||||
this.$emit('onUpdate', {
|
||||
width: this.width * scale,
|
||||
height: this.height * scale,
|
||||
})
|
||||
},
|
||||
handlePanMove(event) {
|
||||
let coordinate
|
||||
if (event.type === 'mousemove') {
|
||||
coordinate = this.handleMousemove(event)
|
||||
}
|
||||
if (event.type === 'touchmove') {
|
||||
coordinate = this.handleTouchmove(event)
|
||||
}
|
||||
|
||||
const _dx = (coordinate.detail.x - this.startX) / this.pageScale
|
||||
const _dy = (coordinate.detail.y - this.startY) / this.pageScale
|
||||
if (this.operation === 'move') {
|
||||
this.dx = _dx
|
||||
this.dy = _dy
|
||||
} else if (this.operation === 'scale') {
|
||||
if (this.directions.includes('left')) {
|
||||
this.dx = _dx
|
||||
this.dw = -_dx
|
||||
}
|
||||
if (this.directions.includes('top')) {
|
||||
this.dy = _dy
|
||||
this.dh = -_dy
|
||||
}
|
||||
if (this.directions.includes('right')) {
|
||||
this.dw = _dx
|
||||
}
|
||||
if (this.directions.includes('bottom')) {
|
||||
this.dh = _dy
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
handlePanEnd(event) {
|
||||
if (event.type === 'mouseup') {
|
||||
this.handleMouseup(event)
|
||||
}
|
||||
if (event.type === 'touchend') {
|
||||
this.handleTouchend(event)
|
||||
}
|
||||
if (this.operation === 'move') {
|
||||
// eslint-disable-next-line vue/custom-event-name-casing
|
||||
this.$emit('onUpdate', {
|
||||
x: this.x + this.dx,
|
||||
y: this.y + this.dy,
|
||||
})
|
||||
this.dx = 0
|
||||
this.dy = 0
|
||||
} else if (this.operation === 'scale') {
|
||||
// eslint-disable-next-line vue/custom-event-name-casing
|
||||
this.$emit('onUpdate', {
|
||||
x: this.x + this.dx,
|
||||
y: this.y + this.dy,
|
||||
width: this.width + this.dw,
|
||||
height: Math.round((this.width + this.dw) / this.ratio),
|
||||
})
|
||||
this.dx = 0
|
||||
this.dy = 0
|
||||
this.dw = 0
|
||||
this.dh = 0
|
||||
this.directions = []
|
||||
}
|
||||
this.operation = ''
|
||||
},
|
||||
handlePanStart(event) {
|
||||
let coordinate
|
||||
if (event.type === 'mousedown') {
|
||||
coordinate = this.handleMousedown(event)
|
||||
}
|
||||
if (event.type === 'touchstart') {
|
||||
coordinate = this.handleTouchStart(event)
|
||||
}
|
||||
if (!coordinate) return
|
||||
|
||||
this.startX = coordinate.detail.x
|
||||
this.startY = coordinate.detail.y
|
||||
if (coordinate.detail.target === event.currentTarget) {
|
||||
return (this.operation = 'move')
|
||||
}
|
||||
this.operation = 'scale'
|
||||
this.directions = coordinate.detail.target.dataset.direction.split('-')
|
||||
},
|
||||
onDelete() {
|
||||
// eslint-disable-next-line vue/custom-event-name-casing
|
||||
this.$emit('onDelete')
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.operation {
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
.content {
|
||||
color: var(--color-text-maxcontrast);
|
||||
}
|
||||
|
||||
.selector {
|
||||
border-radius: 10px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-left: -6px;
|
||||
margin-top: -6px;
|
||||
background-color: #32b5fe;
|
||||
border: 1px solid #32b5fe;
|
||||
}
|
||||
.delete {
|
||||
border-radius: 10px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-left: -9px;
|
||||
margin-top: -9px;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -17,103 +17,78 @@
|
|||
</small>
|
||||
</p>
|
||||
<Sidebar class="view-sign-detail--sidebar"
|
||||
:signers="signers"
|
||||
:signers="document.signers"
|
||||
event="libresign:visible-elements-select-signer">
|
||||
<button v-if="isDraft" class="primary publish-btn" @click="publish">
|
||||
<NcButton v-if="canSave"
|
||||
:type="canSave?'primary':'secondary'"
|
||||
:wide="true"
|
||||
@click="showConfirm = true">
|
||||
{{ t('libresign', 'Request') }}
|
||||
</button>
|
||||
</NcButton>
|
||||
|
||||
<button v-if="canSign" class="primary publish-btn" @click="goToSign">
|
||||
<NcButton v-if="canSign"
|
||||
:type="!canSave?'primary':'secondary'"
|
||||
:wide="true"
|
||||
@click="goToSign">
|
||||
{{ t('libresign', 'Sign') }}
|
||||
</button>
|
||||
</NcButton>
|
||||
</Sidebar>
|
||||
</div>
|
||||
<div class="image-page">
|
||||
<!-- <canvas ref="canvas" :width="page.resolution.w" :height="page.resolution.h" /> -->
|
||||
<!-- <div :style="{ width: `${page.resolution.w}px`, height: `${page.resolution.h}px`, background: 'red' }">
|
||||
<img :src="page.url">
|
||||
</div> -->
|
||||
<PageNavigation v-model="currentSigner.element.coordinates.page"
|
||||
:pages="document.pages"
|
||||
:width="pageDimensions.css.width" />
|
||||
<div class="image-page--main">
|
||||
<div class="image-page--container"
|
||||
:style="{ '--page-img-w': pageDimensions.css.width, '--page-img-h': pageDimensions.css.height }">
|
||||
<DragResize v-if="hasSignerSelected"
|
||||
parent-limitation
|
||||
:is-active="true"
|
||||
:is-resizable="true"
|
||||
:w="currentSigner.element.coordinates.width"
|
||||
:h="currentSigner.element.coordinates.height"
|
||||
:x="currentSigner.element.coordinates.left"
|
||||
:y="currentSigner.element.coordinates.top"
|
||||
@resizing="resize"
|
||||
@dragging="resize">
|
||||
<div class="image-page--element">
|
||||
{{ currentSigner.displayName }}
|
||||
</div>
|
||||
<div class="image-page--action">
|
||||
<button class="primary" @click="saveElement">
|
||||
{{ t('libresign', editingElement ? 'Update' : 'Save') }}
|
||||
</button>
|
||||
</div>
|
||||
</DragResize>
|
||||
<img ref="img" :src="page.url">
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="loading"
|
||||
class="image-page">
|
||||
<NcLoadingIcon :size="64" name="Loading" />
|
||||
<p>{{ t('libresign', 'Loading file') }}</p>
|
||||
</div>
|
||||
<div v-else class="image-page">
|
||||
<PdfEditor ref="pdfEditor"
|
||||
width="100%"
|
||||
height="100%"
|
||||
:file-src="document.file"
|
||||
@pdf-editor:end-init="updateSigners"
|
||||
@pdf-editor:on-delete-signer="onDeleteSigner" />
|
||||
</div>
|
||||
<NcDialog :open.sync="showConfirm"
|
||||
:name="t('libresign', 'Confirm')"
|
||||
:message="t('libresign', 'Request signatures?')">
|
||||
<template #actions>
|
||||
<NcButton type="secondary" @click="showConfirm = false">
|
||||
{{ t('libresign', 'Cancel') }}
|
||||
</NcButton>
|
||||
<NcButton type="primary" @click="save">
|
||||
{{ t('libresign', 'Request') }}
|
||||
</NcButton>
|
||||
</template>
|
||||
</NcDialog>
|
||||
</NcModal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { showError, showSuccess } from '@nextcloud/dialogs'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import axios from '@nextcloud/axios'
|
||||
import { generateOcsUrl } from '@nextcloud/router'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import DragResize from 'vue-drag-resize'
|
||||
import { get, pick, find, map, cloneDeep, isEmpty } from 'lodash-es'
|
||||
import NcModal from '@nextcloud/vue/dist/Components/NcModal.js'
|
||||
import NcDialog from '../Nextcloud/NcDialog/NcDialog.vue'
|
||||
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
|
||||
import { subscribe, unsubscribe } from '@nextcloud/event-bus'
|
||||
import { service as signService, SIGN_STATUS } from '../../domains/sign/index.js'
|
||||
import { SIGN_STATUS } from '../../domains/sign/enum.js'
|
||||
import Sidebar from './SignDetail/partials/Sidebar.vue'
|
||||
import PageNavigation from './SignDetail/partials/PageNavigation.vue'
|
||||
import { showResponseError } from '../../helpers/errors.js'
|
||||
import { SignatureImageDimensions } from '../Draw/index.js'
|
||||
import Chip from '../Chip.vue'
|
||||
|
||||
const emptyElement = () => {
|
||||
return {
|
||||
coordinates: {
|
||||
page: 1,
|
||||
left: 100,
|
||||
top: 100,
|
||||
height: SignatureImageDimensions.height,
|
||||
width: SignatureImageDimensions.width,
|
||||
},
|
||||
elementId: 0,
|
||||
}
|
||||
}
|
||||
|
||||
const emptySignerData = () => ({
|
||||
signed: null,
|
||||
displayName: '',
|
||||
fullName: null,
|
||||
me: false,
|
||||
signRequestId: 0,
|
||||
email: '',
|
||||
element: emptyElement(),
|
||||
})
|
||||
|
||||
const deepCopy = val => JSON.parse(JSON.stringify(val))
|
||||
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
|
||||
import PdfEditor from '../PdfEditor/PdfEditor.vue'
|
||||
|
||||
export default {
|
||||
name: 'VisibleElements',
|
||||
components: {
|
||||
NcModal,
|
||||
DragResize,
|
||||
NcDialog,
|
||||
Sidebar,
|
||||
PageNavigation,
|
||||
Chip,
|
||||
NcButton,
|
||||
NcLoadingIcon,
|
||||
PdfEditor,
|
||||
},
|
||||
props: {
|
||||
file: {
|
||||
|
|
@ -125,67 +100,47 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
canRequestSign: loadState('libresign', 'can_request_sign'),
|
||||
signers: [],
|
||||
document: {
|
||||
id: '',
|
||||
name: '',
|
||||
signers: [],
|
||||
pages: [],
|
||||
visibleElements: [],
|
||||
loading: false,
|
||||
},
|
||||
modal: false,
|
||||
currentSigner: emptySignerData(),
|
||||
showConfirm: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
pageIndex() {
|
||||
return this.currentSigner.element.coordinates.page - 1
|
||||
},
|
||||
canSign() {
|
||||
if (this.status !== SIGN_STATUS.ABLE_TO_SIGN) {
|
||||
return false
|
||||
}
|
||||
|
||||
return !isEmpty(this.signerFileUuid)
|
||||
return (this.document?.settings?.signerFileUuid ?? '').length > 0
|
||||
},
|
||||
canSave() {
|
||||
if (
|
||||
[
|
||||
SIGN_STATUS.DRAFT,
|
||||
SIGN_STATUS.ABLE_TO_SIGN,
|
||||
SIGN_STATUS.PARTIAL_SIGNED,
|
||||
].includes(this.status)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
status() {
|
||||
return Number(get(this.document, 'status', -1))
|
||||
return Number(this.document?.status ?? -1)
|
||||
},
|
||||
statusLabel() {
|
||||
return get(this.document, 'statusText', '')
|
||||
return this.document.statusText
|
||||
},
|
||||
isDraft() {
|
||||
return this.status === SIGN_STATUS.DRAFT
|
||||
},
|
||||
page() {
|
||||
return this.document.pages[this.pageIndex] || {
|
||||
url: '',
|
||||
resolution: {
|
||||
h: 0,
|
||||
w: 0,
|
||||
},
|
||||
}
|
||||
},
|
||||
pageDimensions() {
|
||||
const { w, h } = this.page.resolution
|
||||
return {
|
||||
height: h,
|
||||
width: w,
|
||||
css: {
|
||||
height: `${Math.ceil(h)}px`,
|
||||
width: `${Math.ceil(w)}px`,
|
||||
},
|
||||
}
|
||||
},
|
||||
hasSignerSelected() {
|
||||
return this.currentSigner.signRequestId !== 0
|
||||
},
|
||||
editingElement() {
|
||||
return this.currentSigner.element.elementId > 0
|
||||
},
|
||||
signerFileUuid() {
|
||||
return get(this.document, ['settings', 'signerFileUuid'])
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
subscribe('libresign:show-visible-elements', this.showModal)
|
||||
|
|
@ -214,109 +169,86 @@ export default {
|
|||
return showError(err.message)
|
||||
},
|
||||
updateSigners() {
|
||||
const { signRequestId } = this.currentSigner
|
||||
|
||||
this.currentSigner = emptySignerData()
|
||||
|
||||
const [signers, visibleElements] = deepCopy([this.document.signers, this.document.visibleElements])
|
||||
|
||||
this.signers = map(signers, signer => {
|
||||
const element = find(visibleElements, (el) => {
|
||||
return el.signRequestId === signer.signRequestId
|
||||
})
|
||||
|
||||
const row = {
|
||||
...signer,
|
||||
element: emptyElement(),
|
||||
this.document.signers.forEach(signer => {
|
||||
if (this.document.visibleElements) {
|
||||
this.document.visibleElements.forEach(element => {
|
||||
if (element.signRequestId === signer.signRequestId) {
|
||||
const object = structuredClone(signer)
|
||||
object.element = element
|
||||
this.$refs.pdfEditor.addSigner(object)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (element) {
|
||||
const coordinates = pick(element.coordinates, ['top', 'left', 'width', 'height', 'page'])
|
||||
|
||||
row.element = {
|
||||
elementId: element.elementId,
|
||||
coordinates,
|
||||
}
|
||||
}
|
||||
|
||||
return row
|
||||
})
|
||||
|
||||
if (signRequestId === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const current = this.signers.find(signer => signer.signRequestId === signRequestId)
|
||||
|
||||
this.onSelectSigner({ ...current })
|
||||
},
|
||||
resize(newRect) {
|
||||
const { coordinates } = this.currentSigner.element
|
||||
|
||||
this.currentSigner.element.coordinates = {
|
||||
...coordinates,
|
||||
...newRect,
|
||||
}
|
||||
},
|
||||
onSelectSigner(signer) {
|
||||
const page = this.pageIndex + 1
|
||||
|
||||
this.currentSigner = emptySignerData()
|
||||
this.currentSigner = cloneDeep(signer)
|
||||
|
||||
if (signer.element.elementId === 0) {
|
||||
this.currentSigner.element.coordinates.page = page
|
||||
signer.element = {
|
||||
coordinates: {
|
||||
page: 1,
|
||||
llx: 100,
|
||||
ury: 100,
|
||||
height: SignatureImageDimensions.height,
|
||||
width: SignatureImageDimensions.width,
|
||||
},
|
||||
}
|
||||
this.$refs.pdfEditor.addSigner(signer)
|
||||
},
|
||||
async onDeleteSigner(object) {
|
||||
if (!object.signer.element.elementId) {
|
||||
return
|
||||
}
|
||||
await axios.delete(generateOcsUrl('/apps/libresign/api/v1/file-element/{uuid}/{elementId}', {
|
||||
uuid: this.document.uuid,
|
||||
elementId: object.signer.element.elementId,
|
||||
}))
|
||||
},
|
||||
goToSign() {
|
||||
const route = this.$router.resolve({ name: 'SignPDF', params: { uuid: this.signerFileUuid } })
|
||||
|
||||
window.location.href = route.href
|
||||
},
|
||||
async publish() {
|
||||
const allow = confirm(t('libresign', 'Request signatures?'))
|
||||
|
||||
if (!allow) {
|
||||
return
|
||||
}
|
||||
|
||||
async save() {
|
||||
try {
|
||||
await signService.changeRegisterStatus(this.document.fileId, SIGN_STATUS.ABLE_TO_SIGN)
|
||||
this.loadDocument()
|
||||
const visibleElements = []
|
||||
Object.entries(this.$refs.pdfEditor.$refs.vuePdfEditor.allObjects).forEach(entry => {
|
||||
const [pageNumber, page] = entry
|
||||
page.forEach(function(element) {
|
||||
visibleElements.push({
|
||||
type: 'signature',
|
||||
signRequestId: element.signer.signRequestId,
|
||||
elementId: element.signer.element.elementId,
|
||||
coordinates: {
|
||||
page: parseInt(pageNumber) + 1,
|
||||
width: element.width,
|
||||
height: element.height,
|
||||
llx: element.x,
|
||||
lly: element.y + element.height,
|
||||
ury: element.y,
|
||||
urx: element.x + element.width,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
await axios.patch(generateOcsUrl('/apps/libresign/api/v1/request-signature'), {
|
||||
users: [],
|
||||
uuid: this.file.uuid,
|
||||
visibleElements,
|
||||
status: 0,
|
||||
})
|
||||
this.showConfirm = false
|
||||
this.closeModal()
|
||||
} catch (err) {
|
||||
this.onError(err)
|
||||
}
|
||||
},
|
||||
async loadDocument() {
|
||||
try {
|
||||
this.signers = []
|
||||
this.document = await axios.get(generateOcsUrl(`/apps/libresign/api/v1/file/validate/file_id/${this.file.nodeId}`))
|
||||
this.document = this.document.data
|
||||
this.updateSigners()
|
||||
} catch (err) {
|
||||
this.onError(err)
|
||||
}
|
||||
},
|
||||
async saveElement() {
|
||||
const { element, signRequestId } = this.currentSigner
|
||||
|
||||
const payload = {
|
||||
coordinates: {
|
||||
...element.coordinates,
|
||||
page: element.coordinates.page,
|
||||
},
|
||||
type: 'signature',
|
||||
signRequestId,
|
||||
}
|
||||
|
||||
try {
|
||||
this.editingElement
|
||||
? await axios.patch(generateOcsUrl(`/apps/libresign/api/v1/file-element/${this.document.uuid}/${element.elementId}`), payload)
|
||||
: await axios.post(generateOcsUrl(`/apps/libresign/api/v1/file-element/${this.document.uuid}`), payload)
|
||||
showSuccess(t('libresign', 'Element created'))
|
||||
|
||||
this.loadDocument()
|
||||
this.loading = true
|
||||
const document = await axios.get(generateOcsUrl(`/apps/libresign/api/v1/file/validate/file_id/${this.file.nodeId}`))
|
||||
this.document = document.data
|
||||
this.loading = false
|
||||
} catch (err) {
|
||||
this.loading = false
|
||||
this.onError(err)
|
||||
}
|
||||
},
|
||||
|
|
@ -324,9 +256,17 @@ export default {
|
|||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.image-page {
|
||||
.py-12,.p-5 {
|
||||
all: unset;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sign-details {
|
||||
margin-left: 5px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.view-sign-detail {
|
||||
|
|
@ -334,54 +274,8 @@ export default {
|
|||
width: 300px;
|
||||
}
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.image-page {
|
||||
width: 100%;
|
||||
margin: 0.5em;
|
||||
&--main {
|
||||
position: relative;
|
||||
.button-vue {
|
||||
margin: 4px;
|
||||
}
|
||||
&--element {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
position: absolute;
|
||||
cursor: grab;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
color: #FFF;
|
||||
font-weight: bold;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
}
|
||||
&--action {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
}
|
||||
&--container {
|
||||
border-color: #000;
|
||||
border-style: solid;
|
||||
border-width: thin;
|
||||
width: var(--page-img-w);
|
||||
height: var(--page-img-h);
|
||||
left: 0;
|
||||
top: 0;
|
||||
&, img {
|
||||
user-select: none;
|
||||
outline: 0;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.publish-btn {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -11,17 +11,6 @@ import {
|
|||
*/
|
||||
const buildService = (http) => {
|
||||
return ({
|
||||
|
||||
/**
|
||||
* @param {string} uuid uuid
|
||||
*
|
||||
* @return {*}
|
||||
*/
|
||||
async validateByUUID(uuid) {
|
||||
const { data } = await http.get(generateOcsUrl(`/apps/libresign/api/v1/file/validate/uuid/${uuid}`))
|
||||
|
||||
return data
|
||||
},
|
||||
async signDocument({ fileId, password, elements, code }) {
|
||||
const url = String(fileId).length >= 10
|
||||
? generateOcsUrl(`/apps/libresign/api/v1/sign/uuid/${fileId}`)
|
||||
|
|
@ -37,68 +26,6 @@ const buildService = (http) => {
|
|||
|
||||
return data
|
||||
},
|
||||
/**
|
||||
* @param {string} fileID fileID
|
||||
* @param {string} email email
|
||||
*
|
||||
* @return {*}
|
||||
*/
|
||||
async notifySigner(fileID, email) {
|
||||
const body = {
|
||||
fileId: fileID,
|
||||
signers: [
|
||||
{
|
||||
email,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const { data } = await http.post(generateOcsUrl('/apps/libresign/api/v1/notify/signers'), body)
|
||||
|
||||
return data
|
||||
},
|
||||
/**
|
||||
* @param {string} fileID fileID
|
||||
* @param {string} signerId signerId
|
||||
*
|
||||
* @return {*}
|
||||
*/
|
||||
async removeSigner(fileID, signerId) {
|
||||
const { data } = await http.delete(generateOcsUrl(`/apps/libresign/api/v1/sign/file_id/${fileID}/${signerId}`))
|
||||
|
||||
return data
|
||||
},
|
||||
/**
|
||||
* update sign document register
|
||||
*
|
||||
* @param {string} fileId fileId
|
||||
* @param {Record<string, unknown>} content content
|
||||
*
|
||||
* @return {Promise<unknown>}
|
||||
*/
|
||||
async updateRegister(fileId, content = {}) {
|
||||
const url = generateOcsUrl('/apps/libresign/api/v1/request-signature')
|
||||
|
||||
const body = {
|
||||
file: { fileId },
|
||||
...content,
|
||||
}
|
||||
|
||||
const { data } = await http.patch(url, body)
|
||||
|
||||
return data
|
||||
},
|
||||
/**
|
||||
* change document sign status
|
||||
*
|
||||
* @param {string} fileId fileId
|
||||
* @param {number} status new status
|
||||
*
|
||||
* @return {Promise<unknown>}
|
||||
*/
|
||||
changeRegisterStatus(fileId, status) {
|
||||
return this.updateRegister(fileId, { status })
|
||||
},
|
||||
/**
|
||||
* request sign code
|
||||
*
|
||||
|
|
|
|||
|
|
@ -1,34 +1,44 @@
|
|||
const { merge } = require('webpack-merge')
|
||||
const path = require('path')
|
||||
const webpackConfig = require('@nextcloud/webpack-vue-config')
|
||||
const nextcloudWebpackConfig = require('@nextcloud/webpack-vue-config')
|
||||
|
||||
const config = {
|
||||
entry: {
|
||||
tab: path.resolve(path.join('src', 'tab.js')),
|
||||
settings: path.resolve(path.join('src', 'settings.js')),
|
||||
external: path.resolve(path.join('src', 'external.js')),
|
||||
validation: path.resolve(path.join('src', 'validation.js')),
|
||||
},
|
||||
optimization: process.env.NODE_ENV === 'production'
|
||||
? { chunkIds: 'deterministic' }
|
||||
: {},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(ttf|otf|eot|woff|woff2)$/,
|
||||
use: {
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: 'fonts/[name].[ext]',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
resourceQuery: /raw/,
|
||||
type: 'asset/source',
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = merge(webpackConfig, config)
|
||||
module.exports = merge(nextcloudWebpackConfig, {
|
||||
entry: {
|
||||
tab: path.resolve(path.join('src', 'tab.js')),
|
||||
settings: path.resolve(path.join('src', 'settings.js')),
|
||||
external: path.resolve(path.join('src', 'external.js')),
|
||||
validation: path.resolve(path.join('src', 'validation.js')),
|
||||
},
|
||||
optimization: process.env.NODE_ENV === 'production'
|
||||
? { chunkIds: 'deterministic' }
|
||||
: {},
|
||||
devServer: {
|
||||
port: 3000, // use any port suitable for your configuration
|
||||
host: '0.0.0.0', // to accept connections from outside container
|
||||
},
|
||||
output: {
|
||||
assetModuleFilename: '[name][ext]?v=[contenthash]',
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(ttf|otf|eot|woff|woff2)$/,
|
||||
use: {
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: 'fonts/[name].[ext]',
|
||||
},
|
||||
},
|
||||
},
|
||||
// Load raw SVGs to be able to inject them via v-html
|
||||
{
|
||||
test: /@mdi\/svg/,
|
||||
type: 'asset/source',
|
||||
},
|
||||
{
|
||||
resourceQuery: /raw/,
|
||||
type: 'asset/source',
|
||||
},
|
||||
],
|
||||
}
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue