diff --git a/appinfo/routes.php b/appinfo/routes.php
index 3620e691e..2e884d4db 100644
--- a/appinfo/routes.php
+++ b/appinfo/routes.php
@@ -25,15 +25,11 @@
return [
'routes' => [
- ['name' => 'page#index', 'url' => '/', 'verb' => 'GET'],
-
- // Before /{hash} to avoid conflict
- ['name' => 'page#index', 'url' => '/new', 'verb' => 'GET', 'postfix' => 'create'],
- ['name' => 'page#index', 'url' => '/{hash}/edit', 'verb' => 'GET', 'postfix' => 'edit'],
- ['name' => 'page#index', 'url' => '/{hash}/clone', 'verb' => 'GET', 'postfix' => 'clone'],
- ['name' => 'page#index', 'url' => '/{hash}/results', 'verb' => 'GET', 'postfix' => 'results'],
-
+ // Before /{hash}/{action} to avoid conflict
['name' => 'page#goto_form', 'url' => '/{hash}', 'verb' => 'GET'],
+
+ // As parameters have defaults, this catches all routes from '/' to '/hash/edit'
+ ['name' => 'page#index', 'url' => '/{hash}/{action}', 'verb' => 'GET', 'defaults' => ['hash' => '', 'action' => '']],
],
'ocs' => [
// Forms
@@ -43,6 +39,7 @@
['name' => 'api#cloneForm', 'url' => '/api/v1/form/clone/{id}', 'verb' => 'POST'],
['name' => 'api#updateForm', 'url' => '/api/v1/form/update', 'verb' => 'POST'],
['name' => 'api#deleteForm', 'url' => '/api/v1/form/{id}', 'verb' => 'DELETE'],
+ ['name' => 'api#getSharedForms', 'url' => '/api/v1/shared_forms', 'verb' => 'GET'],
// Questions
['name' => 'api#newQuestion', 'url' => '/api/v1/question', 'verb' => 'POST'],
diff --git a/docs/API.md b/docs/API.md
index 2f53614b9..eff1a1241 100644
--- a/docs/API.md
+++ b/docs/API.md
@@ -46,6 +46,16 @@ Returns condensed objects of all Forms beeing owned by the authenticated user.
]
```
+### List shared Forms
+Returns condensed objects of all Forms, that are shared to the authenticated user via instance ([access-type](DataStructure.md#share-types) `registered` or `selected`) and have not expired yet.
+- Endpoint: `/api/v1/shared_forms`
+- Method: `GET`
+- Parameters: None
+- Response: Array of condensed Form Objects, sorted as newest first, similar to [List owned Forms](#list-owned-forms).
+```
+See above, 'List owned forms'
+```
+
### Create a new Form
- Endpoint: `/api/v1/form`
- Method: `POST`
@@ -65,7 +75,8 @@ Returns condensed objects of all Forms beeing owned by the authenticated user.
"expires": null,
"isAnonymous": null,
"submitOnce": true,
- "questions": []
+ "questions": [],
+ "canSubmit": true
}
```
diff --git a/docs/DataStructure.md b/docs/DataStructure.md
index e12150098..3a015b152 100644
--- a/docs/DataStructure.md
+++ b/docs/DataStructure.md
@@ -19,6 +19,7 @@ This document describes the Obect-Structure, that is used within the Forms App a
| submitOnce | Boolean | | If users are only allowed to submit once to the form |
| questions | Array of [Questions](#question) | | Array of questions belonging to the form |
| submissions | Array of [Submissions](#submissions) | | Array of submissions belonging to the form |
+| canSubmit | Boolean | | If the user can Submit to the form, i.e. calculated information out of `submitOnce` and existing submissions. |
```
{
@@ -33,7 +34,8 @@ This document describes the Obect-Structure, that is used within the Forms App a
"isAnonymous": false,
"submitOnce": false,
"questions": [],
- "submissions": []
+ "submissions": [],
+ "canSubmit": true
}
```
diff --git a/lib/Controller/ApiController.php b/lib/Controller/ApiController.php
index 30c081a29..6786cf47f 100644
--- a/lib/Controller/ApiController.php
+++ b/lib/Controller/ApiController.php
@@ -149,7 +149,8 @@ public function __construct(string $appName,
/**
* @NoAdminRequired
*
- * Read Form-List only with necessary information for Listing.
+ * Read Form-List of owned forms
+ * Return only with necessary information for Listing.
* @return DataResponse
*/
public function getForms(): DataResponse {
@@ -169,6 +170,38 @@ public function getForms(): DataResponse {
return new DataResponse($result);
}
+ /**
+ * @NoAdminRequired
+ *
+ * Read List of forms shared with current user
+ * Return only with necessary information for Listing.
+ * @return DataResponse
+ */
+ public function getSharedForms(): DataResponse {
+ $forms = $this->formMapper->findAll();
+
+ $result = [];
+ foreach ($forms as $form) {
+ // Don't add if user is owner, user has no access, form has expired, form is link-shared
+ if ($form->getOwnerId() === $this->currentUser->getUID()
+ || !$this->formsService->hasUserAccess($form->getId())
+ || $this->formsService->hasFormExpired($form->getId())
+ || $form->getAccess()['type'] === 'public') {
+ continue;
+ }
+
+ $result[] = [
+ 'id' => $form->getId(),
+ 'hash' => $form->getHash(),
+ 'title' => $form->getTitle(),
+ 'expires' => $form->getExpires(),
+ 'partial' => true
+ ];
+ }
+
+ return new DataResponse($result);
+ }
+
/**
* @NoAdminRequired
*
@@ -914,8 +947,8 @@ public function insertSubmission(int $formId, array $answers): DataResponse {
throw new OCSForbiddenException('Not allowed to access this form');
}
- // Not allowed if form expired. Expires is '0' if the form does not expire.
- if ($form->getExpires() && $form->getExpires() < time()) {
+ // Not allowed if form has expired.
+ if ($this->formsService->hasFormExpired($form->getId())) {
throw new OCSForbiddenException('This form is no longer taking answers');
}
diff --git a/lib/Controller/PageController.php b/lib/Controller/PageController.php
index c6da21222..dc7d811b9 100644
--- a/lib/Controller/PageController.php
+++ b/lib/Controller/PageController.php
@@ -34,12 +34,15 @@
use OCP\AppFramework\Controller;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Http\Template\PublicTemplateResponse;
+use OCP\AppFramework\Http\Response;
use OCP\AppFramework\Http\TemplateResponse;
+use OCP\AppFramework\Http\RedirectResponse;
use OCP\IGroupManager;
use OCP\IInitialStateService;
use OCP\IL10N;
use OCP\ILogger;
use OCP\IRequest;
+use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserManager;
use OCP\IUserSession;
@@ -74,6 +77,9 @@ class PageController extends Controller {
/** @var ILogger */
private $logger;
+ /** @var IUrlGenerator */
+ private $urlGenerator;
+
/** @var IUserManager */
private $userManager;
@@ -101,6 +107,7 @@ public function __construct(string $appName,
IInitialStateService $initialStateService,
IL10N $l10n,
ILogger $logger,
+ IUrlGenerator $urlGenerator,
IUserManager $userManager,
IUserSession $userSession) {
parent::__construct($appName, $request);
@@ -115,6 +122,7 @@ public function __construct(string $appName,
$this->initialStateService = $initialStateService;
$this->l10n = $l10n;
$this->logger = $logger;
+ $this->urlGenerator = $urlGenerator;
$this->userManager = $userManager;
$this->userSession = $userSession;
}
@@ -137,9 +145,9 @@ public function index(): TemplateResponse {
* @NoCSRFRequired
* @PublicPage
* @param string $hash
- * @return TemplateResponse
+ * @return RedirectResponse|TemplateResponse Redirect for logged-in users, public template otherwise.
*/
- public function gotoForm($hash): ?TemplateResponse {
+ public function gotoForm(string $hash): Response {
// Inject style on all templates
Util::addStyle($this->appName, 'forms');
@@ -149,18 +157,21 @@ public function gotoForm($hash): ?TemplateResponse {
return $this->provideTemplate(self::TEMPLATE_NOTFOUND);
}
- // Does the user have access to form
- if (!$this->formsService->hasUserAccess($form->getId())) {
- return $this->provideTemplate(self::TEMPLATE_NOTFOUND);
- }
+ // If not link-shared, redirect to internal route
+ if ($form->getAccess()['type'] !== 'public') {
+ $internalLink = $this->urlGenerator->linkToRoute('forms.page.index', ['hash' => $hash, 'action' => 'submit']);
- // Does the user have permissions to submit (resp. submitOnce)
- if (!$this->formsService->canSubmit($form->getId())) {
- return $this->provideTemplate(self::TEMPLATE_NOSUBMIT, $form);
+ if ($this->userSession->isLoggedIn()) {
+ // Directly internal view
+ return new RedirectResponse($internalLink);
+ } else {
+ // Internal through login
+ return new RedirectResponse($this->urlGenerator->linkToRoute('core.login.showLoginForm', ['redirect_url' => $internalLink]));
+ }
}
// Has form expired
- if ($form->getExpires() !== 0 && time() > $form->getExpires()) {
+ if ($this->formsService->hasFormExpired($form->getId())) {
return $this->provideTemplate(self::TEMPLATE_EXPIRED, $form);
}
diff --git a/lib/Service/FormsService.php b/lib/Service/FormsService.php
index afef7974e..23311fd94 100644
--- a/lib/Service/FormsService.php
+++ b/lib/Service/FormsService.php
@@ -148,14 +148,15 @@ public function getForm(int $id): array {
$result['questions'] = $this->getQuestions($id);
// Set proper user/groups properties
-
// Make sure we have the bare minimum
$result['access'] = array_merge(['users' => [], 'groups' => []], $result['access']);
-
// Properly format users & groups
$result['access']['users'] = array_map([$this, 'formatUsers'], $result['access']['users']);
$result['access']['groups'] = array_map([$this, 'formatGroups'], $result['access']['groups']);
+ // Append canSubmit, to be able to show proper EmptyContent on internal view.
+ $result['canSubmit'] = $this->canSubmit($form->getId());
+
return $result;
}
@@ -188,6 +189,11 @@ public function canSubmit($formId) {
return true;
}
+ // Owner is always allowed to submit
+ if ($this->currentUser->getUID() === $form->getOwnerId()) {
+ return true;
+ }
+
// Refuse access, if SubmitOnce is set and user already has taken part.
if ($form->getSubmitOnce()) {
$participants = $this->submissionMapper->findParticipantsByForm($form->getId());
@@ -248,6 +254,17 @@ public function hasUserAccess(int $formId): bool {
return false;
}
+ /*
+ * Has the form expired?
+ *
+ * @param int $formId The id of the form to check.
+ * @return boolean
+ */
+ public function hasFormExpired(int $formId): bool {
+ $form = $this->formMapper->findById($formId);
+ return ($form->getExpires() !== 0 && $form->getExpires() < time());
+ }
+
/**
* Format users access
*
diff --git a/src/Forms.vue b/src/Forms.vue
index 2f1ad59fd..e52a355f0 100644
--- a/src/Forms.vue
+++ b/src/Forms.vue
@@ -26,17 +26,28 @@
+
+
+
+
+
+
-
+
{{ t('forms', 'Loading forms …') }}
@@ -80,6 +91,7 @@ import axios from '@nextcloud/axios'
import AppContent from '@nextcloud/vue/dist/Components/AppContent'
import AppNavigation from '@nextcloud/vue/dist/Components/AppNavigation'
+import AppNavigationCaption from '@nextcloud/vue/dist/Components/AppNavigationCaption'
import AppNavigationNew from '@nextcloud/vue/dist/Components/AppNavigationNew'
import Content from '@nextcloud/vue/dist/Components/Content'
import isMobile from '@nextcloud/vue/src/mixins/isMobile'
@@ -95,6 +107,7 @@ export default {
AppNavigationForm,
AppContent,
AppNavigation,
+ AppNavigationCaption,
AppNavigationNew,
Content,
EmptyContent,
@@ -107,26 +120,61 @@ export default {
loading: true,
sidebarOpened: false,
forms: [],
+ sharedForms: [],
}
},
computed: {
noForms() {
- return this.forms && this.forms.length === 0
+ return this.noOwnedForms && this.noSharedForms
+ },
+ noOwnedForms() {
+ return this.forms?.length === 0
+ },
+ noSharedForms() {
+ return this.sharedForms?.length === 0
},
routeHash() {
return this.$route.params.hash
},
+ // If the user is allowed to access this route
+ routeAllowed() {
+ // If the form is not within the owned or shared list, the user has no access on internal view.
+ if (this.routeHash && this.forms.concat(this.sharedForms).findIndex(form => form.hash === this.routeHash) < 0) {
+ console.error('Form not found for hash: ', this.routeHash)
+ return false
+ }
+
+ // For Routes edit or results, the form must be in owned forms.
+ if (this.$route.name === 'edit' || this.$route.name === 'results') {
+ if (this.forms.findIndex(form => form.hash === this.routeHash) < 0) {
+ return false
+ }
+ }
+
+ return true
+ },
+
selectedForm: {
get() {
- return this.forms.find(form => form.hash === this.routeHash)
+ if (this.routeAllowed) {
+ return this.forms.concat(this.sharedForms).find(form => form.hash === this.routeHash)
+ }
+ return {}
},
set(form) {
- const index = this.forms.findIndex(search => search.hash === this.routeHash)
+ // If a owned form
+ let index = this.forms.findIndex(search => search.hash === this.routeHash)
if (index > -1) {
this.$set(this.forms, index, form)
+ return
+ }
+ // Otherwise a shared form
+ index = this.sharedForms.findIndex(search => search.hash === this.routeHash)
+ if (index > -1) {
+ this.$set(this.sharedForms, index, form)
}
},
},
@@ -151,15 +199,26 @@ export default {
*/
async loadForms() {
this.loading = true
+
+ // Load Owned forms
try {
const response = await axios.get(generateOcsUrl('apps/forms/api/v1', 2) + 'forms')
this.forms = OcsResponse2Data(response)
} catch (error) {
showError(t('forms', 'An error occurred while loading the forms list'))
console.error(error)
- } finally {
- this.loading = false
}
+
+ // Load shared forms
+ try {
+ const response = await axios.get(generateOcsUrl('apps/forms/api/v1', 2) + 'shared_forms')
+ this.sharedForms = OcsResponse2Data(response)
+ } catch (error) {
+ showError(t('forms', 'An error occurred while loading the forms list'))
+ console.error(error)
+ }
+
+ this.loading = false
},
/**
diff --git a/src/FormsSubmit.vue b/src/FormsSubmit.vue
new file mode 100644
index 000000000..0f8fad424
--- /dev/null
+++ b/src/FormsSubmit.vue
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/components/AppNavigationForm.vue b/src/components/AppNavigationForm.vue
index 4a03787cd..43627ffae 100644
--- a/src/components/AppNavigationForm.vue
+++ b/src/components/AppNavigationForm.vue
@@ -25,9 +25,12 @@
ref="navigationItem"
:icon="icon"
:title="formTitle"
- :to="{ name: 'formRoot', params: { hash: form.hash } }"
+ :to="{
+ name: routerTarget,
+ params: { hash: form.hash }
+ }"
@click="mobileCloseNavigation">
-
+
{},
+ }
},
methods: {
+ /**
+ * Focus title after form load
+ */
+ focusTitle() {
+ this.$nextTick(() => {
+ this.$refs.title.focus()
+ })
+ },
+
+ /**
+ * Fetch the full form data and update parent
+ *
+ * @param {number} id the unique form hash
+ */
+ async fetchFullForm(id) {
+ this.isLoadingForm = true
+
+ // Cancel previous request
+ this.cancelFetchFullForm('New request pending.')
+
+ // Output after cancelling previous request for logical order.
+ console.debug('Loading form', id)
+
+ // Create new cancelable get request
+ const { request, cancel } = CancelableRequest(async function(url, requestOptions) {
+ return axios.get(url, requestOptions)
+ })
+ // Store cancel-function
+ this.cancelFetchFullForm = cancel
+
+ try {
+ const response = await request(generateOcsUrl('apps/forms/api/v1', 2) + `form/${id}`)
+ this.$emit('update:form', OcsResponse2Data(response))
+ this.isLoadingForm = false
+ } catch (error) {
+ if (axios.isCancel(error)) {
+ console.debug('The request for form', id, 'has been canceled.', error)
+ } else {
+ console.error(error)
+ this.isLoadingForm = false
+ }
+ } finally {
+ if (this.form.title === '') {
+ this.focusTitle()
+ }
+ }
+ },
+
async saveFormProperty(key) {
try {
// TODO: add loading status feedback ?
diff --git a/src/router.js b/src/router.js
index b4423d7d6..cb742754c 100644
--- a/src/router.js
+++ b/src/router.js
@@ -29,6 +29,7 @@ import { generateUrl } from '@nextcloud/router'
import Create from './views/Create'
import Results from './views/Results'
import Sidebar from './views/Sidebar'
+import Submit from './views/Submit'
Vue.use(Router)
@@ -66,5 +67,11 @@ export default new Router({
name: 'results',
props: true,
},
+ {
+ path: '/:hash/submit',
+ component: Submit,
+ name: 'submit',
+ props: true,
+ },
],
})
diff --git a/src/submit.js b/src/submit.js
index 8175f1525..23d798dc0 100644
--- a/src/submit.js
+++ b/src/submit.js
@@ -23,7 +23,7 @@
import { translate, translatePlural } from '@nextcloud/l10n'
import Vue from 'vue'
-import Fill from './views/Submit'
+import FormsSubmitRoot from './FormsSubmit'
Vue.prototype.t = translate
Vue.prototype.n = translatePlural
@@ -32,5 +32,5 @@ export default new Vue({
el: '#content',
// eslint-disable-next-line vue/match-component-file-name
name: 'FormsSubmitRoot',
- render: h => h(Fill),
+ render: h => h(FormsSubmitRoot),
})
diff --git a/src/views/Create.vue b/src/views/Create.vue
index cfdb39675..0321cc54d 100644
--- a/src/views/Create.vue
+++ b/src/views/Create.vue
@@ -138,7 +138,6 @@ import Actions from '@nextcloud/vue/dist/Components/Actions'
import AppContent from '@nextcloud/vue/dist/Components/AppContent'
import answerTypes from '../models/AnswerTypes'
-import CancelableRequest from '../utils/CancelableRequest'
import EmptyContent from '../components/EmptyContent'
import Question from '../components/Questions/Question'
import QuestionLong from '../components/Questions/QuestionLong'
@@ -183,14 +182,8 @@ export default {
answerTypes,
// Various states
- isLoadingForm: true,
isLoadingQuestions: false,
- errorForm: false,
-
isDragging: false,
-
- // storage for axios cancel function
- cancelFetchFullForm: () => {},
}
},
@@ -250,55 +243,6 @@ export default {
},
methods: {
- /**
- * Fetch the full form data and update parent
- *
- * @param {number} id the unique form hash
- */
- async fetchFullForm(id) {
- this.isLoadingForm = true
-
- // Cancel previous request
- this.cancelFetchFullForm('New request pending.')
-
- // Output after cancelling previous request for logical order.
- console.debug('Loading form', id)
-
- // Create new cancelable get request
- const { request, cancel } = CancelableRequest(async function(url, requestOptions) {
- return axios.get(url, requestOptions)
- })
- // Store cancel-function
- this.cancelFetchFullForm = cancel
-
- try {
- const response = await request(generateOcsUrl('apps/forms/api/v1', 2) + `form/${id}`)
- this.$emit('update:form', OcsResponse2Data(response))
- this.isLoadingForm = false
- } catch (error) {
- if (axios.isCancel(error)) {
- console.debug('The request for form', id, 'has been canceled.', error)
- } else {
- console.error(error)
- this.errorForm = true
- this.isLoadingForm = false
- }
- } finally {
- if (this.form.title === '') {
- this.focusTitle()
- }
- }
- },
-
- /**
- * Focus title after form load
- */
- focusTitle() {
- this.$nextTick(() => {
- this.$refs.title.focus()
- })
- },
-
/**
* Title & description save methods
*/
diff --git a/src/views/Submit.vue b/src/views/Submit.vue
index c3db3bc1a..ad5a86ceb 100644
--- a/src/views/Submit.vue
+++ b/src/views/Submit.vue
@@ -21,56 +21,59 @@
-->
-
-
-
-
-
-
-
- {{ form.description }}
-
-
-
-
-
-
-
-
- {{ t('forms', 'Submitting form …') }}
-
-
-
- {{ t('forms', 'Thank you for completing the form!') }}
-
-
-
+
+
+ {{ t('forms', 'Loading {title} …', { title: form.title }) }}
+
+
+
+
+
+
+
+
+
+ {{ form.description }}
+
+
+
+
+
+ {{ t('forms', 'Submitting form …') }}
+
+
+ {{ t('forms', 'Thank you for completing the form!') }}
+
+
+
+
+