Skip to content

Commit b24065c

Browse files
committed
feat: add import function (#1425)
Signed-off-by: TimedIn <git@timedin.net>
1 parent b68fd98 commit b24065c

5 files changed

Lines changed: 159 additions & 21 deletions

File tree

lib/Controller/ApiController.php

Lines changed: 42 additions & 16 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);
187-
188-
// Read old form, (un)set new form specific data, extend title
189-
$formData = $oldForm->read();
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();
198+
} else {
199+
$questions = $form['questions'];
200+
$oldConfirmationEmailQuestionId = $form['confirmationEmailQuestionId'];
201+
unset($form['questions']);
202+
$formData = $form;
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,
@@ -213,26 +229,36 @@ public function newForm(?int $fromId = null): DataResponse {
213229
$form = Form::fromParams($formData);
214230
$this->formMapper->insert($form);
215231

216-
// Get Questions, set new formId, reinsert
217-
$questions = $this->questionMapper->findByForm($oldForm->getId());
218-
$oldConfirmationEmailQuestionId = $oldForm->getConfirmationEmailQuestionId();
219-
220232
foreach ($questions as $oldQuestion) {
221-
$questionData = $oldQuestion->read();
233+
if ($fromId !== null) {
234+
$questionData = $oldQuestion->read();
235+
$oldQuestionId = $oldQuestion->getId();
236+
// Get Options, set new QuestionId, reinsert
237+
$options = $this->optionMapper->findByQuestion($oldQuestionId);
238+
} else {
239+
$questionData = $oldQuestion;
240+
$oldQuestionId = $oldQuestion['id'];
241+
$options = $oldQuestion["options"];
242+
}
222243

223244
unset($questionData['id']);
245+
unset($questionData['options']);
246+
unset($questionData['accept']);
247+
224248
$questionData['formId'] = $form->getId();
225249
$newQuestion = Question::fromParams($questionData);
226250
$this->questionMapper->insert($newQuestion);
227251

228-
if (isset($oldConfirmationEmailQuestionId) && $oldConfirmationEmailQuestionId === $oldQuestion->getId()) {
252+
if (isset($oldConfirmationEmailQuestionId) && $oldConfirmationEmailQuestionId === $oldQuestionId) {
229253
$form->setConfirmationEmailQuestionId($newQuestion->getId());
230254
}
231255

232-
// Get Options, set new QuestionId, reinsert
233-
$options = $this->optionMapper->findByQuestion($oldQuestion->getId());
234256
foreach ($options as $oldOption) {
235-
$optionData = $oldOption->read();
257+
if ($fromId !== null) {
258+
$optionData = $oldOption->read();
259+
} else {
260+
$optionData = $oldOption;
261+
}
236262

237263
unset($optionData['id']);
238264
$optionData['questionId'] = $newQuestion->getId();

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="showVersionMismatch"
154+
contentClasses="modal-content"
155+
:name="t('forms', 'Version mismatch')"
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 newer 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 showVersionMismatch = 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+
showVersionMismatch.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+
showVersionMismatch.value = true
524+
} else {
525+
await onImportForm()
526+
}
527+
})
528+
reader.readAsText(file)
529+
})
530+
}
531+
532+
const closeModal = () => {
533+
showVersionMismatch.value = false
534+
formForImport = undefined
535+
}
446536
const onDownloadForm = async (id) => {
447537
try {
448538
const response = await axios.get(
@@ -550,6 +640,7 @@ export default {
550640
forms,
551641
allSharedForms,
552642
showArchivedForms,
643+
showVersionMismatch,
553644
canCreateForms,
554645
allowComments,
555646
isMobile,
@@ -569,10 +660,14 @@ export default {
569660
onNewForm,
570661
onCloneForm,
571662
onDownloadForm,
663+
onUploadForm,
572664
onDeleteForm,
665+
onImportForm,
666+
closeModal,
573667
onLastUpdatedByEventBus,
574668
IconPlus,
575669
IconArchive,
670+
IconUpload,
576671
FormsIcon,
577672
}
578673
},

0 commit comments

Comments
 (0)