Skip to content

Commit

Permalink
feat(WikiSettings): Add QuestyCaptcha card to Wiki Settings (#760)
Browse files Browse the repository at this point in the history
* feat(WikiSettings): Add QuestyCaptcha card to Wiki Settings

* fix: remove dropdown arrow from combobox

* fix: move default questions to store

* feat: add error messages for empty fields

* fix: Fix recover default questions button

* clean up testing codes

* fix: Fix validation

* fix: fix some specs and behaviors to match design

* fix: passing wikiId prop to fix saving issue in prod

* fix: delete QA bundle before saving

* fix: add more condition to delete empty QA bundle with save button

* fix: fix saving behavior

* fix: fix UI specs

* change: reduced answers input field width to 95%

* fix: change answer field margin

* fix: remove some unwanted css used inline css

* fix: change input field width conditionally

* Fix missing "panel" property error

* Display messages even if the panel is collapsed

* Switch to inline validation for v-combobox

* Simplify showing the delete button

* Reuse the existing v-snackbar

* Minor documentation cleanup

* Switch to using async/await

* Fix fetching of existing questions

* Scroll to first invalid field on error

* Synchronously update local settings

* Captcha toggle should work independently

* Fix linting errors

* Fix hidden validation errors

---------

Co-authored-by: Andrew Kostka <[email protected]>
  • Loading branch information
dati18 and AndrewKostka authored Jan 25, 2024
1 parent fdffa77 commit 4e270cd
Show file tree
Hide file tree
Showing 3 changed files with 279 additions and 1 deletion.
242 changes: 242 additions & 0 deletions src/components/Pages/ManageWiki/Cards/QuestyCaptcha.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
<template>
<v-card>
<v-card-title>Additional Spam Protection</v-card-title>
<v-card-text class="pb-2">
QuestyCaptcha offers an extra layer of protection against spam accounts. During account creation, users will have to answer a question, which can be defined in settings. For more information on QuestyCaptcha, please visit the documentation page
</v-card-text>
<v-col class="switch">
<v-switch
label="Activate spam protection"
v-model="isCaptchaActive"
@change="toggleCaptcha"
:loading="waitForToggleUpdate"
:disabled="waitForToggleUpdate"
/>
</v-col>
<v-expansion-panels v-model="panel">
<v-expansion-panel>
<v-expansion-panel-header>
<strong>SETTINGS</strong>
</v-expansion-panel-header>
<v-expansion-panel-content>
<strong>Guidelines for creating questions and answers:</strong><br/>
<ul>
<li>Keep in mind that users have a wide variety of knowledge and abilities. We therefore recommend three questions at minimum, each requiring different abilities or knowledge.</li>
<li>Consider cultural bias. Use questions that rely on universal knowledge or knowledge related to your wiki.</li>
<li>Keep the answers short and simple. Ideally, try to use questions with only one possible answer.</li>
</ul>
<v-form ref="questyForm">
<div class="pt-10" v-for="(entry, index) in questionsFromStore" :key="index">
Question
<v-text-field
class="trash-icon pb-2"
v-model="entry.question"
outlined
hide-details="auto"
:append-outer-icon="showDeleteButton ? 'mdi-delete-outline' : undefined"
:rules="[() => !!entry.question || 'Field cannot be empty. Please provide a question.']"
@click:append-outer="removeQuestion(index)"
dense
:disabled="waitForQuestionsUpdate"
></v-text-field>
Answer
<v-combobox
:class="{'answer-box': true, 'answer-input-field': showDeleteButton}"
v-model="entry.answers"
:items="entry.answers"
multiple
outlined
:rules="[() => !!entry.answers.length || 'Field cannot be empty. Please provide an answer.']"
hide-selected
hide-details="auto"
dense
:disabled="waitForQuestionsUpdate"
>
<template v-slot:selection="{ item }" >
<v-chip class="chips" :disabled="waitForQuestionsUpdate">
<span class="pr-1">
{{ item }}
</span>
<v-icon
small
@click="removeAnswer(entry, item)"
:disabled="waitForQuestionsUpdate"
>
mdi-close-circle
</v-icon>
</v-chip>
</template>
</v-combobox>
</div>
<div class="d-flex pb-12 pt-10">
<v-btn @click="addQuestion" :disabled="waitForQuestionsUpdate" elevation=0 plain class="ml-auto">+ ADD QUESTION</v-btn>
</div>
<div>
<v-btn @click="saveForm" :disabled="waitForQuestionsUpdate" color="primary" width="100%">SAVE QUESTIONS</v-btn>
</div>
<div class="pt-4">
<v-btn @click="recoverDefaultQuestions" :disabled="waitForQuestionsUpdate" elevation=0 width="100%">RECOVER DEFAULT QUESTIONS</v-btn>
</div>
</v-form>
</v-expansion-panel-content>
</v-expansion-panel>
</v-expansion-panels>
<v-snackbar :color="message.status" elevation="24" v-model="message.show">
{{ message.text }}
<template v-slot:action>
<v-btn
text
variant="text"
@click="message.show = false"
>
Close
</v-btn>
</template>
</v-snackbar>
</v-card>
</template>

<script>
export default {
name: 'QuestyCaptcha',
props: [
'wikiId'
],
data () {
return {
message: false,
questionsFromStore: [],
defaultQuestions: [],
isCaptchaActive: false,
hasNoQuestions: false,
panel: false,
waitForToggleUpdate: false,
waitForQuestionsUpdate: false
}
},
computed: {
showDeleteButton: function () {
return this.questionsFromStore.length > 1
}
},
created () {
this.defaultQuestions = this.$store.state.wikis.currentWikiSettings.defaultQuestions
this.isCaptchaActive = this.$store.state.wikis.currentWikiSettings.wwUseQuestyCaptcha
this.questionsFromStore = this.$store.state.wikis.currentWikiSettings.captchaQuestions
this.hasNoQuestions = !this.questionsFromStore
if (this.hasNoQuestions) {
this.recoverDefaultQuestions()
}
},
methods: {
removeAnswer (question, answer) {
const index = question.answers.indexOf(answer)
if (index !== -1) {
question.answers.splice(index, 1)
}
},
removeQuestion (index) {
this.questionsFromStore.splice(index, 1)
},
addQuestion () {
this.questionsFromStore.push({
question: '',
answers: []
})
},
showMessage (status, message) {
this.message = { status: status, text: message, show: true }
},
formatQuestionsForApi (questions) {
return JSON.stringify(questions.reduce((out, entry) => {
out[entry.question] = entry.answers
return out
}, {}))
},
async toggleCaptcha (enabled) {
try {
this.waitForToggleUpdate = true
if (enabled && this.hasNoQuestions) {
await this.$store.dispatch('updateSetting', {
wiki: this.wikiId, setting: 'wwCaptchaQuestions', value: this.formatQuestionsForApi(this.defaultQuestions)
})
await this.$store.dispatch('setQuestyCaptchaQuestions', this.defaultQuestions)
this.hasNoQuestions = false
}
await this.$store.dispatch('updateSetting', { wiki: this.wikiId, setting: 'wwUseQuestyCaptcha', value: enabled })
await this.$store.dispatch('setEnabledQuestyCaptcha', enabled)
this.showMessage('success', `QuestyCaptcha has been successfully ${enabled ? 'enabled' : 'disabled'}.`)
} catch (error) {
console.log(error.response)
this.showMessage('error', `Something went wrong while ${enabled ? 'enabling' : 'disabling'} QuestyCaptcha. Please try again.`)
await this.$nextTick()
this.isCaptchaActive = !enabled
} finally {
this.waitForToggleUpdate = false
}
},
async saveForm () {
this.waitForQuestionsUpdate = true
for (let i = 0; i < this.questionsFromStore.length; i++) {
const entry = this.questionsFromStore[i]
const noQuestion = entry.question.trim() === ''
const noAnswer = entry.answers && entry.answers.length === 0
if (noQuestion && noAnswer && this.questionsFromStore.length > 1) {
this.questionsFromStore.splice(i, 1)
}
}
await this.$nextTick()
this.$refs.questyForm.validate()
const invalidField = this.$refs.questyForm.$children.find((field) => {
return typeof field.validate === 'function' && !field.validate()
})
if (invalidField) {
invalidField.$el.scrollIntoView({ behavior: 'smooth' })
this.waitForQuestionsUpdate = false
return
}
try {
await this.$store.dispatch('updateSetting', {
wiki: this.wikiId, setting: 'wwCaptchaQuestions', value: this.formatQuestionsForApi(this.questionsFromStore)
})
await this.$store.dispatch('setQuestyCaptchaQuestions', this.questionsFromStore)
this.showMessage('success', 'Your questions have been saved.')
this.hasNoQuestions = false
this.panel = false
} catch (error) {
console.log(error.response)
this.showMessage('error', 'Something went wrong with saving your questions. Please try again.')
} finally {
this.waitForQuestionsUpdate = false
}
},
recoverDefaultQuestions () {
// parse() and stringify() are being used to make a copy
this.questionsFromStore = JSON.parse(JSON.stringify(this.defaultQuestions))
}
}
}
</script>

<style lang="css" scoped>
.switch {
padding-left: 20px;
padding-bottom: 0;
padding-top: 0;
}
.answer-input-field {
margin-right: 33px !important;
}
>>> .answer-box .v-input__append-inner {
display: none !important;
}
.chips {
margin: 0 8px 0 0 !important;
}
>>> .trash-icon .v-input__append-outer {
margin-top: 0 !important;
}
</style>
7 changes: 7 additions & 0 deletions src/components/Pages/ManageWiki/Tabs/ManageWiki.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
<Logo :wikiId="this.wikiId"/>
</v-col>
</v-row>
<v-row>
<v-col>
<QuestyCaptcha :wikiId="this.wikiId"/>
</v-col>
</v-row>
</v-col>
<!--Col 2-->
<v-col>
Expand Down Expand Up @@ -62,10 +67,12 @@ import Skin from '~/components/Pages/ManageWiki/Cards/Skin'
import Registration from '~/components/Pages/ManageWiki/Cards/Registration'
import Wikibase from '~/components/Pages/ManageWiki/Cards/Wikibase'
import Delete from '~/components/Pages/ManageWiki/Cards/Delete'
import QuestyCaptcha from '../Cards/QuestyCaptcha'
export default {
name: 'ManageWiki',
components: {
QuestyCaptcha,
Details,
Logo,
Skin,
Expand Down
31 changes: 30 additions & 1 deletion src/store/wikis.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,20 @@ const mutations = {
const defaultMapping = { properties: { P31: MAPPING_SUGGESTION_PLACEHOLDER, P279: MAPPING_SUGGESTION_PLACEHOLDER }, items: {} }
const wikibaseManifestEquivEntities = entityMappingSetting ? JSON.parse(entityMappingSetting.value) : defaultMapping

const wwUseQuestyCaptchaSetting = details.public_settings.find(setting => setting.name === 'wwUseQuestyCaptcha')
const wwUseQuestyCaptcha = wwUseQuestyCaptchaSetting ? parseInt(wwUseQuestyCaptchaSetting.value) === 1 : false

const captchaQuestionsSetting = details.public_settings.find(setting => setting.name === 'wwCaptchaQuestions')
const defaultQuestions = [
{ question: 'How many vowels are in this question?', answers: ['12', 'twelve'] },
{ question: 'What is the chemical formula of water?', answers: ['H2O'] },
{ question: '2 + 4 = ?', answers: ['6', 'six'] }
]
const captchaQuestions = captchaQuestionsSetting
? Object.entries(JSON.parse(captchaQuestionsSetting.value)).map(([key, value]) => {
return { question: key, answers: value }
}) : undefined

const federatedPropertiesSetting = details.public_settings.find(setting => setting.name === 'wikibaseFedPropsEnable')
const wikibaseFedPropsEnable = federatedPropertiesSetting ? parseInt(federatedPropertiesSetting.value) === 1 : false

Expand Down Expand Up @@ -90,7 +104,10 @@ const mutations = {
wwWikibaseStringLengthString,
wwWikibaseStringLengthMonolingualText,
wwWikibaseStringLengthMultilang,
wwExtEnableConfirmAccount
wwExtEnableConfirmAccount,
wwUseQuestyCaptcha,
captchaQuestions,
defaultQuestions
}
},
clear_current_wiki_settings (state) {
Expand Down Expand Up @@ -119,6 +136,12 @@ const mutations = {
},
set_enable_confirm_account (state, { value }) {
state.currentWikiSettings.wwExtEnableConfirmAccount = value
},
set_enable_questy_captcha (state, { value }) {
state.currentWikiSettings.wwUseQuestyCaptcha = value
},
set_questy_captcha_questions (state, value) {
state.currentWikiSettings.captchaQuestions = value
}
}

Expand Down Expand Up @@ -186,6 +209,12 @@ const actions = {
items: filterOutPlaceholderMapping(mapping.items)
})
})
},
setEnabledQuestyCaptcha ({ commit }, enabled) {
commit('set_enable_questy_captcha', enabled)
},
setQuestyCaptchaQuestions ({ commit }, value) {
commit('set_questy_captcha_questions', value)
}
}

Expand Down

0 comments on commit 4e270cd

Please sign in to comment.