Skip to content

Commit

Permalink
feat: Add form state handling (e.g. archived) to the frontend
Browse files Browse the repository at this point in the history
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
  • Loading branch information
susnux committed Feb 18, 2024
1 parent 0661ce7 commit 8d08024
Show file tree
Hide file tree
Showing 9 changed files with 429 additions and 228 deletions.
130 changes: 100 additions & 30 deletions src/Forms.vue
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@
</NcAppNavigationNew>
<template #list>
<!-- Form-Owner-->
<NcAppNavigationCaption v-if="!noOwnedForms" :name="t('forms', 'Your Forms')" />
<AppNavigationForm v-for="form in forms"
<NcAppNavigationCaption v-if="ownedForms.length > 0" :name="t('forms', 'Your Forms')" />
<AppNavigationForm v-for="form in ownedForms"
:key="form.id"
:form="form"
:read-only="false"
Expand All @@ -44,13 +44,35 @@
@delete="onDeleteForm" />

<!-- Shared Forms-->
<NcAppNavigationCaption v-if="!noSharedForms" :name="t('forms', 'Shared with you')" />
<NcAppNavigationCaption v-if="sharedForms.length > 0" :name="t('forms', 'Shared with you')" />
<AppNavigationForm v-for="form in sharedForms"
:key="form.id"
:form="form"
:read-only="true"
@open-sharing="openSharing"
@mobile-close-navigation="mobileCloseNavigation" />

<!-- Archived Forms-->
<NcAppNavigationItem v-if="archivedForms.length > 0"
allow-collapse
:name="t('forms', 'Archived forms')"
:open.sync="showArchivedForms">
<template #icon>
<IconLock :size="20" />
</template>
<template #counter>
{{ archivedForms.length > 99 ? '99+' : archivedForms.length }}
</template>
<template #default>
<AppNavigationForm v-for="form in archivedForms"
:key="form.id"
:form="form"
:read-only="true"
class="forms-archived"
@open-sharing="openSharing"
@mobile-close-navigation="mobileCloseNavigation" />
</template>
</NcAppNavigationItem>
</template>
</NcAppNavigation>

Expand Down Expand Up @@ -117,30 +139,35 @@ import { useIsMobile } from '@nextcloud/vue'
import NcAppContent from '@nextcloud/vue/dist/Components/NcAppContent.js'
import NcAppNavigation from '@nextcloud/vue/dist/Components/NcAppNavigation.js'
import NcAppNavigationCaption from '@nextcloud/vue/dist/Components/NcAppNavigationCaption.js'
import NcAppNavigationItem from '@nextcloud/vue/dist/Components/NcAppNavigationItem.js'
import NcAppNavigationNew from '@nextcloud/vue/dist/Components/NcAppNavigationNew.js'
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import NcContent from '@nextcloud/vue/dist/Components/NcContent.js'
import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
import IconLock from 'vue-material-design-icons/Lock.vue'
import IconPlus from 'vue-material-design-icons/Plus.vue'
import FormsIcon from './components/Icons/FormsIcon.vue'
import AppNavigationForm from './components/AppNavigationForm.vue'
import PermissionTypes from './mixins/PermissionTypes.js'
import OcsResponse2Data from './utils/OcsResponse2Data.js'
import logger from './utils/Logger.js'
import { FormState } from './models/FormStates.ts'
export default {
name: 'Forms',
components: {
AppNavigationForm,
FormsIcon,
IconLock,
IconPlus,
NcAppContent,
NcAppNavigation,
NcAppNavigationCaption,
NcAppNavigationItem,
NcAppNavigationNew,
NcButton,
NcContent,
Expand All @@ -162,7 +189,8 @@ export default {
sidebarOpened: false,
sidebarActive: 'forms-sharing',
forms: [],
sharedForms: [],
allSharedForms: [],
showArchivedForms: false,
canCreateForms: loadState(appName, 'appConfig').canCreateForms,
}
Expand All @@ -172,14 +200,33 @@ export default {
canEdit() {
return this.selectedForm.permissions.includes(this.PERMISSION_TYPES.PERMISSION_EDIT)
},
hasForms() {
return !this.noOwnedForms || !this.noSharedForms
return this.allSharedForms.length > 0 || this.forms.length > 0
},
noOwnedForms() {
return this.forms?.length === 0
/**
* All own forms that are not archived
*/
ownedForms() {
return this.forms.filter((form) => form.state !== FormState.FormArchived)
},
noSharedForms() {
return this.sharedForms?.length === 0
/**
* All shared forms that are not archived
*/
sharedForms() {
return this.allSharedForms.filter((form) => form.state !== FormState.FormArchived)
},
/**
* All forms that have been archived
*/
archivedForms() {
return [
...this.forms,
...this.allSharedForms,
].filter((form) => form.state === FormState.FormArchived)
},
routeHash() {
Expand All @@ -194,7 +241,7 @@ export default {
}
// Try to find form in owned & shared list
const form = [...this.forms, ...this.sharedForms]
const form = [...this.forms, ...this.allSharedForms]
.find(form => form.hash === this.routeHash)
// If no form found, load it from server. Route will be automatically re-evaluated.
Expand All @@ -210,7 +257,7 @@ export default {
selectedForm: {
get() {
if (this.routeAllowed) {
return this.forms.concat(this.sharedForms).find(form => form.hash === this.routeHash)
return this.forms.concat(this.allSharedForms).find(form => form.hash === this.routeHash)
}
return {}
},
Expand All @@ -222,14 +269,20 @@ export default {
return
}
// Otherwise a shared form
index = this.sharedForms.findIndex(search => search.hash === this.routeHash)
index = this.allSharedForms.findIndex(search => search.hash === this.routeHash)
if (index > -1) {
this.$set(this.sharedForms, index, form)
this.$set(this.allSharedForms, index, form)
}
},
},
},
watch: {
selectedForm(form) {
this.showArchivedForms = form.state === FormState.FormArchived
},
},
beforeMount() {
this.loadForms()
},
Expand Down Expand Up @@ -285,7 +338,7 @@ export default {
// Load shared forms
try {
const response = await axios.get(generateOcsUrl('apps/forms/api/v2.4/shared_forms'))
this.sharedForms = OcsResponse2Data(response)
this.allSharedForms = OcsResponse2Data(response)
} catch (error) {
logger.error('Error while loading shared forms list', { error })
showError(t('forms', 'An error occurred while loading the forms list'))
Expand All @@ -300,22 +353,34 @@ export default {
* @param {string} hash the hash of the form to load
*/
async fetchPartialForm(hash) {
this.loading = true
try {
const response = await axios.get(generateOcsUrl('apps/forms/api/v2.4/partial_form/{hash}', { hash }))
const form = OcsResponse2Data(response)
await new Promise((resolve) => {
const wait = () => {
if (this.loading) {
window.setTimeout(wait, 250)
} else {
resolve()
}
}
wait()
})
// If the user has (at least) submission-permissions, add it to the shared forms
if (form.permissions.includes(this.PERMISSION_TYPES.PERMISSION_SUBMIT)) {
this.sharedForms.push(form)
this.loading = true
if ([...this.forms, ...this.allSharedForms].find((form) => form.hash === hash) === undefined) {
try {
const response = await axios.get(generateOcsUrl('apps/forms/api/v2.4/partial_form/{hash}', { hash }))
const form = OcsResponse2Data(response)
// If the user has (at least) submission-permissions, add it to the shared forms
if (form.permissions.includes(this.PERMISSION_TYPES.PERMISSION_SUBMIT)) {
this.allSharedForms.push(form)
}
} catch (error) {
logger.error(`Form ${hash} not found`, { error })
showError(t('forms', 'Form not found'))
}
} catch (error) {
logger.error(`Form ${hash} not found`, { error })
showError(t('forms', 'Form not found'))
} finally {
this.loading = false
}
this.loading = false
},
/**
Expand Down Expand Up @@ -381,16 +446,21 @@ export default {
this.forms[formIndex].lastUpdated = moment().unix()
this.forms.sort((b, a) => a.lastUpdated - b.lastUpdated)
} else {
const sharedFormIndex = this.sharedForms.findIndex(form => form.id === id)
this.sharedForms[sharedFormIndex].lastUpdated = moment().unix()
this.sharedForms.sort((b, a) => a.lastUpdated - b.lastUpdated)
const sharedFormIndex = this.allSharedForms.findIndex(form => form.id === id)
this.allSharedForms[sharedFormIndex].lastUpdated = moment().unix()
this.allSharedForms.sort((b, a) => a.lastUpdated - b.lastUpdated)
}
},
},
}
</script>
<style scoped lang="scss">
.forms-archived {
padding-inline-start: 16px;
}
.forms-emptycontent {
height: 100%;
}
Expand Down
5 changes: 5 additions & 0 deletions src/components/AppNavigationForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ import IconDeleteSvg from '@mdi/svg/svg/delete.svg?raw'
import FormsIcon from './Icons/FormsIcon.vue'
import { FormState } from '../models/FormStates'
import logger from '../utils/Logger.js'
export default {
Expand Down Expand Up @@ -186,6 +187,10 @@ export default {
* Return expiration details for subtitle
*/
formSubtitle() {
if (this.form.state === FormState.FormClosed) {
// TRANSLATORS: The form was closed manually so it does not take new submissions
return t('forms', 'Form closed')
}
if (this.form.expires) {
const relativeDate = moment(this.form.expires, 'X').fromNow()
if (this.isExpired) {
Expand Down
52 changes: 49 additions & 3 deletions src/components/SidebarTabs/SettingsSidebarTab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
-
- @author John Molakvoæ <skjnldsv@protonmail.com>
- @author Jonas Rittershofer <jotoeri@users.noreply.github.com>
- @author Ferdinand Thiessen <opensource@fthiessen.de>
-
- @license AGPL-3.0-or-later
-
Expand All @@ -24,24 +25,26 @@
<template>
<div class="sidebar-tabs__content">
<NcCheckboxRadioSwitch :checked="form.isAnonymous"
:disabled="formArchived"
type="switch"
@update:checked="onAnonChange">
<!-- TRANSLATORS Checkbox to select whether responses will be stored anonymously or not -->
{{ t('forms', 'Store responses anonymously') }}
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch v-tooltip="disableSubmitMultipleExplanation"
:checked="submitMultiple"
:disabled="disableSubmitMultiple"
:disabled="disableSubmitMultiple || formArchived"
type="switch"
@update:checked="onSubmitMultipleChange">
{{ t('forms', 'Allow multiple responses per person') }}
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch :checked="formExpires"
:disabled="formArchived"
type="switch"
@update:checked="onFormExpiresChange">
{{ t('forms', 'Set expiration date') }}
</NcCheckboxRadioSwitch>
<div v-show="formExpires" class="settings-div--indent">
<div v-show="formExpires && !formArchived" class="settings-div--indent">
<NcDateTimePicker id="expiresDatetimePicker"
:clearable="false"
:disabled-date="notBeforeToday"
Expand All @@ -59,7 +62,27 @@
{{ t('forms', 'Show expiration date on form') }}
</NcCheckboxRadioSwitch>
</div>
<NcCheckboxRadioSwitch :checked="formClosed"
:disabled="formArchived"
aria-describedby="forms-settings__close-form"
type="switch"
@update:checked="onFormClosedChange">
{{ t('forms', 'Close form') }}
</NcCheckboxRadioSwitch>
<p id="forms-settings__close-form" class="settings-hint">
{{ t('forms', 'Closed forms to not accept new submissions.') }}
</p>
<NcCheckboxRadioSwitch :checked="formArchived"
aria-describedby="forms-settings__archive-form"
type="switch"
@update:checked="onFormArchivedChange">
{{ t('forms', 'Archive form') }}
</NcCheckboxRadioSwitch>
<p id="forms-settings__archive-form" class="settings-hint">
{{ t('forms', 'Archived forms to not accept new submissions and can not be modified.') }}
</p>
<NcCheckboxRadioSwitch :checked="hasCustomSubmissionMessage"
:disabled="formArchived"
type="switch"
@update:checked="onUpdateHasCustomSubmissionMessage">
{{ t('forms', 'Custom submission message') }}
Expand All @@ -68,7 +91,7 @@
class="settings-div--indent submission-message"
:tabindex="editMessage ? undefined : '0'"
@focus="editMessage = true">
<textarea v-if="editMessage || !form.submissionMessage"
<textarea v-if="!formArchived && (editMessage || !form.submissionMessage)"
v-click-outside="() => { editMessage = false }"
aria-describedby="forms-submission-message-description"
:aria-label="t('forms', 'Custom submission message')"
Expand Down Expand Up @@ -102,6 +125,7 @@ import TransferOwnership from './TransferOwnership.vue'
import { directive as ClickOutside } from 'v-click-outside'
import { loadState } from '@nextcloud/initial-state'
import { FormState } from '../../models/FormStates.ts'
export default {
components: {
Expand Down Expand Up @@ -171,6 +195,15 @@ export default {
formExpires() {
return this.form.expires !== 0
},
formArchived() {
return this.form.state === FormState.FormArchived
},
formClosed() {
return this.form.state !== FormState.FormActive
},
isExpired() {
return this.form.expires && moment().unix() > this.form.expires
},
Expand Down Expand Up @@ -217,6 +250,14 @@ export default {
this.$emit('update:formProp', 'expires', parseInt(moment(datetime).format('X')))
},
onFormClosedChange(isClosed) {
this.$emit('update:formProp', 'state', isClosed ? FormState.FormClosed : FormState.FormActive)
},
onFormArchivedChange(isArchived) {
this.$emit('update:formProp', 'state', isArchived ? FormState.FormArchived : FormState.FormClosed)
},
onSubmissionMessageChange({ target }) {
this.$emit('update:formProp', 'submissionMessage', target.value)
},
Expand Down Expand Up @@ -290,6 +331,11 @@ export default {
margin-inline-start: 40px;
}
.settings-hint {
color: var(--color-text-maxcontrast);
padding-inline-start: 16px;
}
.sidebar-tabs__content {
display: flex;
flex-direction: column;
Expand Down
Loading

0 comments on commit 8d08024

Please sign in to comment.