Skip to content

Commit 5a575cf

Browse files
committed
feat: add import function
1 parent 87bfc7c commit 5a575cf

6 files changed

Lines changed: 142 additions & 88 deletions

File tree

lib/Controller/ApiController.php

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,8 @@ public function getForms(string $type = 'owned'): DataResponse {
148148
* Return a copy of the form if the parameter $fromId is set
149149
*
150150
* @param ?int $fromId (optional) Id of the form that should be cloned
151+
* @param ?bool $import (optional) If it should import the form from post body
152+
* @param ?array<string, mixed> $form (optional) The formdata to import
151153
* @return DataResponse<Http::STATUS_CREATED, FormsForm, array{}>
152154
* @throws OCSForbiddenException The user is not allowed to create forms
153155
*
@@ -157,14 +159,14 @@ public function getForms(string $type = 'owned'): DataResponse {
157159
#[NoAdminRequired()]
158160
#[BruteForceProtection(action: 'form')]
159161
#[ApiRoute(verb: 'POST', url: '/api/v3/forms')]
160-
public function newForm(?int $fromId = null): DataResponse {
162+
public function newForm(?int $fromId = null, ?bool $import = false, ?array $form = []): DataResponse {
161163
// Check if user is allowed
162164
if (!$this->configService->canCreateForms()) {
163165
$this->logger->debug('This user is not allowed to create Forms.');
164166
throw new OCSForbiddenException('This user is not allowed to create Forms.');
165167
}
166168

167-
if ($fromId === null) {
169+
if ($fromId === null && $import === false) {
168170
// Create Form
169171
$form = new Form();
170172
$form->setOwnerId($this->currentUser->getUID());
@@ -183,10 +185,22 @@ public function newForm(?int $fromId = null): DataResponse {
183185

184186
$this->formMapper->insert($form);
185187
} else {
186-
$oldForm = $this->formsService->getFormIfAllowed($fromId, Constants::PERMISSION_EDIT);
188+
$formData = [];
189+
$questions = [];
190+
if($fromId !== null) {
191+
$oldForm = $this->formsService->getFormIfAllowed($fromId, Constants::PERMISSION_EDIT);
192+
193+
// Read old form, (un)set new form specific data, extend title
194+
$formData = $oldForm->read();
195+
// Get Questions, set new formId, reinsert
196+
$questions = $this->questionMapper->findByForm($oldForm->getId());
197+
$oldConfirmationEmailQuestionId = $oldForm->getConfirmationEmailQuestionId();
187198

188-
// Read old form, (un)set new form specific data, extend title
189-
$formData = $oldForm->read();
199+
} else {
200+
$formData = $form;
201+
$questions = $form["questions"];
202+
$oldConfirmationEmailQuestionId = $form["confirmationEmailQuestionId"];
203+
}
190204
unset($formData['id']);
191205
unset($formData['created']);
192206
unset($formData['lastUpdated']);
@@ -199,7 +213,9 @@ public function newForm(?int $fromId = null): DataResponse {
199213
$formData['ownerId'] = $this->currentUser->getUID();
200214
$formData['hash'] = $this->formsService->generateFormHash();
201215
// TRANSLATORS Appendix to the form Title of a duplicated/copied form.
202-
$formData['title'] .= ' - ' . $this->l10n->t('Copy');
216+
if($fromId !== null) {
217+
$formData['title'] .= ' - ' . $this->l10n->t('Copy');
218+
}
203219
$formData['access'] = [
204220
'permitAllUsers' => false,
205221
'showToAllUsers' => false,
@@ -209,13 +225,11 @@ public function newForm(?int $fromId = null): DataResponse {
209225
$formData['showExpiration'] = false;
210226
$formData['expires'] = 0;
211227
$formData['isAnonymous'] = false;
212-
228+
229+
unset($formData['questions']);
213230
$form = Form::fromParams($formData);
214231
$this->formMapper->insert($form);
215-
216-
// Get Questions, set new formId, reinsert
217-
$questions = $this->questionMapper->findByForm($oldForm->getId());
218-
$oldConfirmationEmailQuestionId = $oldForm->getConfirmationEmailQuestionId();
232+
219233

220234
foreach ($questions as $oldQuestion) {
221235
$questionData = $oldQuestion->read();

openapi.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -869,6 +869,21 @@
869869
"nullable": true,
870870
"default": null,
871871
"description": "(optional) Id of the form that should be cloned"
872+
},
873+
"import": {
874+
"type": "boolean",
875+
"nullable": true,
876+
"default": false,
877+
"description": "(optional) If it should import the form from post body"
878+
},
879+
"form": {
880+
"type": "object",
881+
"nullable": true,
882+
"default": {},
883+
"description": "(optional) The formdata to import",
884+
"additionalProperties": {
885+
"type": "object"
886+
}
872887
}
873888
}
874889
}

package-lock.json

Lines changed: 4 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"markdown-it": "^14.2.0",
4646
"p-queue": "^9.3.0",
4747
"qrcode": "^1.5.4",
48+
"semver": "^7.8.2",
4849
"vue": "^3.5.22",
4950
"vue-draggable-plus": "^0.6.1",
5051
"vue-router": "^4.6.4"

src/Forms.vue

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,16 @@
2323
isHeading
2424
class="forms-navigation__list-heading"
2525
headingId="forms-navigation-your-forms"
26-
:name="t('forms', 'Your forms')" />
26+
:name="t('forms', 'Your forms')">
27+
<template #actions>
28+
<NcActionButton v-if="true" @click="onUploadForm()">
29+
<template #icon>
30+
<NcIconSvgWrapper :svg="IconUpload" />
31+
</template>
32+
{{ t('calendar', 'Import form') }}
33+
</NcActionButton>
34+
</template>
35+
</NcAppNavigationCaption>
2736
<ul aria-labelledby="forms-navigation-your-forms">
2837
<AppNavigationForm
2938
v-for="form in ownedForms"
@@ -139,6 +148,31 @@
139148
@update:active="sidebarActive = $event" />
140149
</template>
141150

151+
<!-- Import form modal -->
152+
<NcDialog
153+
v-model:open="showVersionMissmatch"
154+
contentClasses="modal-content"
155+
:name="t('forms', 'Version missmatch')"
156+
outTransition
157+
@close="closeModal">
158+
<template #default>
159+
<!-- eslint-disable vue/no-v-html -->
160+
<p>
161+
{{
162+
t(
163+
'forms',
164+
'The version of the uploaded form is never than the installed app version. Do you still want to import the form?',
165+
)
166+
}}
167+
</p>
168+
</template>
169+
<template #actions>
170+
<NcButton variant="error" @click="onImportForm">
171+
{{ t('forms', 'I understand, import this form') }}
172+
</NcButton>
173+
</template>
174+
</NcDialog>
175+
142176
<!-- Archived forms modal -->
143177
<ArchivedFormsModal
144178
v-model:open="showArchivedForms"
@@ -150,13 +184,15 @@
150184
<script>
151185
import IconPlus from '@material-symbols/svg-400/outlined/add.svg?raw'
152186
import IconArchive from '@material-symbols/svg-400/outlined/archive.svg?raw'
187+
import IconUpload from '@material-symbols/svg-400/outlined/upload.svg?raw'
153188
import axios from '@nextcloud/axios'
154189
import { showError } from '@nextcloud/dialogs'
155190
import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'
156191
import { loadState } from '@nextcloud/initial-state'
157192
import moment from '@nextcloud/moment'
158193
import { generateOcsUrl } from '@nextcloud/router'
159-
import { useIsMobile } from '@nextcloud/vue'
194+
import { NcActionButton, NcDialog, useIsMobile } from '@nextcloud/vue'
195+
import semverCompare from 'semver/functions/compare'
160196
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
161197
import { useRoute, useRouter } from 'vue-router'
162198
import NcAppContent from '@nextcloud/vue/components/NcAppContent'
@@ -195,6 +231,8 @@ export default {
195231
NcButton,
196232
NcContent,
197233
NcEmptyContent,
234+
NcActionButton,
235+
NcDialog,
198236
NcLoadingIcon,
199237
Sidebar,
200238
},
@@ -210,6 +248,8 @@ export default {
210248
const forms = ref([])
211249
const allSharedForms = ref([])
212250
const showArchivedForms = ref(false)
251+
const showVersionMissmatch = ref(false)
252+
let formForImport = undefined
213253
const canCreateForms = ref(loadState(appName, 'appConfig').canCreateForms)
214254
const allowComments = ref(loadState(appName, 'appConfig').allowComments)
215255
const deletedFormHash = ref(null)
@@ -443,6 +483,56 @@ export default {
443483
}
444484
}
445485
486+
const onImportForm = async () => {
487+
showVersionMissmatch.value = false
488+
try {
489+
const response = await axios.post(
490+
generateOcsUrl('apps/forms/api/v3/forms?import=1'),
491+
{ form: formForImport },
492+
)
493+
const newForm = OcsResponse2Data(response)
494+
forms.value.unshift(newForm)
495+
router.push({
496+
name: 'edit',
497+
params: { hash: newForm.hash },
498+
})
499+
mobileCloseNavigation()
500+
} catch (error) {
501+
logger.error(`Unable to import form`, { error })
502+
showError(t('forms', 'Unable to import form'))
503+
}
504+
}
505+
506+
const onUploadForm = () => {
507+
// Open file pickers
508+
const fileInput = document.createElement('input')
509+
fileInput.type = 'file'
510+
fileInput.accept = 'application/json'
511+
fileInput.click()
512+
513+
fileInput.addEventListener('change', () => {
514+
const file = fileInput.files[0]
515+
if (file.type !== 'application/json' || file.size > 1000 * 1000)
516+
return
517+
const reader = new FileReader()
518+
reader.addEventListener('load', async () => {
519+
const formObject = JSON.parse(reader.result)
520+
if (!formObject.appVersion || !formObject.form) return
521+
formForImport = formObject.form
522+
if (semverCompare(version, formObject.appVersion) === -1) {
523+
showVersionMissmatch.value = true
524+
} else {
525+
await onImportForm()
526+
}
527+
})
528+
reader.readAsText(file)
529+
})
530+
}
531+
532+
const closeModal = () => {
533+
showVersionMissmatch.value = false
534+
formForImport = undefined
535+
}
446536
const onDownloadForm = async (id) => {
447537
const response = await axios.get(
448538
generateOcsUrl('apps/forms/api/v3/forms/{id}', {
@@ -552,6 +642,7 @@ export default {
552642
forms,
553643
allSharedForms,
554644
showArchivedForms,
645+
showVersionMissmatch,
555646
canCreateForms,
556647
allowComments,
557648
isMobile,
@@ -571,10 +662,14 @@ export default {
571662
onNewForm,
572663
onCloneForm,
573664
onDownloadForm,
665+
onUploadForm,
574666
onDeleteForm,
667+
onImportForm,
668+
closeModal,
575669
onLastUpdatedByEventBus,
576670
IconPlus,
577671
IconArchive,
672+
IconUpload,
578673
FormsIcon,
579674
}
580675
},

src/components/SidebarTabs/TransferOwnership.vue

Lines changed: 0 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -16,78 +16,6 @@
1616
t('forms', 'Transfer ownership')
1717
}}</span>
1818
</NcButton>
19-
20-
<NcDialog
21-
v-model:open="showModal"
22-
contentClasses="modal-content"
23-
:name="t('forms', 'Transfer ownership')"
24-
outTransition
25-
@close="closeModal">
26-
<template #default>
27-
<!-- eslint-disable vue/no-v-html -->
28-
<p
29-
v-html="
30-
t(
31-
'forms',
32-
'You\'re going to transfer the ownership of {name} to another account. Please select the account to which you want to transfer ownership.',
33-
{
34-
name: `<strong>${escapedString(form.title)}</strong>`,
35-
},
36-
undefined,
37-
{ escape: false },
38-
)
39-
" />
40-
<!-- eslint-enable vue/no-v-html -->
41-
<NcSelectUsers
42-
v-model="selected"
43-
class="modal-content__select"
44-
:loading="loading"
45-
:options="options"
46-
:placeholder="t('forms', 'Search for a user')"
47-
@search="
48-
(query) => asyncSearch(query, [SHARE_TYPES.SHARE_TYPE_USER])
49-
">
50-
<template #no-options>
51-
{{ noResultText }}
52-
</template>
53-
</NcSelectUsers>
54-
55-
<br />
56-
57-
<!-- eslint-disable vue/no-v-html -->
58-
<p
59-
v-html="
60-
t(
61-
'forms',
62-
'Type {text} to confirm.',
63-
{
64-
text: `<strong>${escapedString(confirmationString)}</strong>`,
65-
},
66-
undefined,
67-
{ escape: false },
68-
)
69-
" />
70-
<!-- eslint-enable vue/no-v-html -->
71-
<NcTextField
72-
v-model="confirmationInput"
73-
:label="t('forms', 'Confirmation text')"
74-
:success="confirmationInput === confirmationString" />
75-
76-
<br />
77-
78-
<p>
79-
<strong>{{ t('forms', 'This can not be undone.') }}</strong>
80-
</p>
81-
</template>
82-
<template #actions>
83-
<NcButton
84-
:disabled="!canTransfer"
85-
variant="error"
86-
@click="onOwnershipTransfer">
87-
{{ t('forms', 'I understand, transfer this form') }}
88-
</NcButton>
89-
</template>
90-
</NcDialog>
9119
</div>
9220
</template>
9321

0 commit comments

Comments
 (0)