Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(WikiSettings): Add QuestyCaptcha card to Wiki Settings #760

Merged
merged 30 commits into from
Jan 25, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
669461f
feat(WikiSettings): Add QuestyCaptcha card to Wiki Settings
dati18 Dec 8, 2023
cb2c71f
fix: remove dropdown arrow from combobox
dati18 Dec 19, 2023
ae1ff28
fix: move default questions to store
dati18 Dec 22, 2023
b29f8cf
feat: add error messages for empty fields
dati18 Jan 4, 2024
3ba9b1c
fix: Fix recover default questions button
dati18 Jan 8, 2024
77f2c51
clean up testing codes
dati18 Jan 8, 2024
132994e
fix: Fix validation
dati18 Jan 9, 2024
5c1fbbc
fix: fix some specs and behaviors to match design
dati18 Jan 10, 2024
0af47f9
fix: passing wikiId prop to fix saving issue in prod
dati18 Jan 10, 2024
3096695
fix: delete QA bundle before saving
dati18 Jan 10, 2024
cde17ae
fix: add more condition to delete empty QA bundle with save button
dati18 Jan 10, 2024
fb1de61
fix: fix saving behavior
dati18 Jan 11, 2024
2261d3e
fix: fix UI specs
dati18 Jan 11, 2024
fad3f5e
change: reduced answers input field width to 95%
dati18 Jan 11, 2024
1da694a
fix: change answer field margin
dati18 Jan 11, 2024
909d581
fix: remove some unwanted css used inline css
dati18 Jan 12, 2024
1dd0255
fix: change input field width conditionally
dati18 Jan 12, 2024
f3ff79d
Fix missing "panel" property error
AndrewKostka Jan 16, 2024
4c00bcb
Display messages even if the panel is collapsed
AndrewKostka Jan 16, 2024
3f24b32
Switch to inline validation for v-combobox
AndrewKostka Jan 16, 2024
9a7fd67
Simplify showing the delete button
AndrewKostka Jan 16, 2024
c4e7526
Reuse the existing v-snackbar
AndrewKostka Jan 16, 2024
7ff3442
Minor documentation cleanup
AndrewKostka Jan 16, 2024
ec228ae
Switch to using async/await
AndrewKostka Jan 16, 2024
bf6cf41
Fix fetching of existing questions
AndrewKostka Jan 19, 2024
c9c8149
Scroll to first invalid field on error
AndrewKostka Jan 19, 2024
e10cb35
Synchronously update local settings
AndrewKostka Jan 19, 2024
1435654
Captcha toggle should work independently
AndrewKostka Jan 19, 2024
48fcb89
Fix linting errors
AndrewKostka Jan 19, 2024
d4f3143
Fix hidden validation errors
AndrewKostka Jan 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
235 changes: 235 additions & 0 deletions src/components/Pages/ManageWiki/Cards/QuestyCaptcha.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
<template>
<v-card>
<v-card-title>Additional Spam Protection</v-card-title>
<v-card-text class="text">
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="checkbox">
<v-checkbox label="Activate spam protection" v-model="captchaActivate"></v-checkbox>
</v-col>
<!-- Setting Panel-->
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can totally see why you added these comments but in general this might be a sign that in future it would be best to split up this single one large card into multiple smaller components that could also have similarly descriptive names. Let's not change this now though

<v-expansion-panels>
<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>
<!-- Question/Answer Bundle-->
<v-form ref="questyForm">
<div class="pt-10" v-for="(entry, index) in questionsFromStore" :key="index">
Question
<v-text-field
class="input-field trash-icon text-field"
v-model="entry.question"
outlined
hide-details="auto"
:append-outer-icon="showIcon ? 'mdi-delete-outline' : undefined"
:rules="[() => !!entry.question || 'Field cannot be empty. Please provide a question']"
@click:append-outer="removeQuestion(index)"
></v-text-field>
Answer
<v-combobox
class="answer-box input-field answer-input-field"
v-model="entry.answers"
:items="entry.answers"
multiple
outlined
:rules="[required]"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it might be slightly clearer to either inline this rule or change the method name to something more descriptive than required

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any suggestions?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

hide-selected
hide-details="auto"
>
<template v-slot:selection="{ item }" >
<v-chip class="chips">
<span class="pr-1">
{{ item }}
</span>
<v-icon
small
@click="removeAnswer(entry, item)"
>
mdi-close-circle
</v-icon>
</v-chip>
</template>
</v-combobox>
</div>
<!-- Buttons-->
<div class="d-flex pb-12 pt-10">
<v-btn @click="addQuestion" elevation=0 plain class="ml-auto">+ ADD QUESTION</v-btn>
</div>
<div>
<v-btn @click="saveForm" color="primary" width="100%">SAVE QUESTIONS</v-btn>
</div>
<div class="pt-4">
<v-btn @click="recoverDefaultQuestions" elevation=0 width="100%">RECOVER DEFAULT QUESTIONS</v-btn>
</div>
</v-form>
<!-- Success/Error Message Snackbar-->
<v-snackbar color="success" elevation="24" v-model="successMessage">
Your questions have been saved
<template v-slot:action>
<v-btn
text
variant="text"
@click="closeAlert"
>
Close
</v-btn>
</template>
</v-snackbar>
<v-snackbar color="error" elevation="24" v-model="errorMessage">
Something is wrong with saving your questions. Please try again
<template v-slot:action>
<v-btn
text
variant="text"
@click="closeAlert"
>
Close
</v-btn>
</template>
</v-snackbar>
</v-expansion-panel-content>
</v-expansion-panel>
</v-expansion-panels>
</v-card>
</template>

<script>

export default {
name: 'QuestyCaptcha',
props: [
'wikiId'
],
data () {
return {
successMessage: false,
errorMessage: false,
captchaActivate: false,
questionsFromStore: [],
showIcon: true
}
},
created () {
this.questionsFromStore = this.$store.state.wikis.currentWikiSettings.captchaQuestions
this.captchaActivate = this.$store.state.wikis.currentWikiSettings.wwUseQuestyCaptcha
},
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)
// hide trash icon when there is just one QA bundle
if (this.questionsFromStore.length === 1) {
this.showIcon = false
}
},
addQuestion () {
this.questionsFromStore.push({
question: '',
answers: []
})
// show the trash icon again when there are more than one QA bundle
this.showIcon = true
},
saveForm () {
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)
}
}
this.$nextTick(() => {
if (!this.$refs.questyForm.validate()) {
console.log('validation failss')
return
}
const wiki = this.wikiId
const promises = []
const captchaEnabledSetting = 'wwUseQuestyCaptcha'
const captchaQuestionsSetting = 'wwCaptchaQuestions'
const enableValue = this.captchaActivate
const questions = {}
this.questionsFromStore.forEach(item => {
questions[item.question] = item.answers
})
const JSONQuestions = JSON.stringify(questions)
promises.push(
this.$store.dispatch('updateSetting', { wiki, setting: captchaEnabledSetting, value: enableValue }),
this.$store.dispatch('updateSetting', { wiki, setting: captchaQuestionsSetting, value: JSONQuestions })
)
Promise.all(promises)
.then(() => {
this.$store.dispatch('setEnabledQuestyCaptcha', enableValue)
this.$store.dispatch('setQuestyCaptchaQuestions', this.questionsFromStore)
this.successMessage = true
})
.catch(err => {
console.log(err.response)
this.errorMessage = true
})
})
},
recoverDefaultQuestions () {
const recoveredDefaultQuestions = this.$store.state.wikis.currentWikiSettings.defaultQuestions
this.questionsFromStore = JSON.parse(JSON.stringify(recoveredDefaultQuestions))
},
required (value) {
if (value.length === 0) {
return 'Field cannot be empty. Please provide an answer'
}
return !!value || 'Field cannot be empty. Please provide an answer'
},
closeAlert () {
this.successMessage = false
this.errorMessage = false
}
}
}
</script>

<style lang="css" scoped>
AndrewKostka marked this conversation as resolved.
Show resolved Hide resolved
.checkbox {
padding-left: 20px;
padding-bottom: 0;
padding-top: 0;
}
>>> .input-field .v-input__slot {
min-height: 40px !important;
}
>>> .answer-input-field .v-input__slot {
width: 95% !important;
}
>>> .answer-box .v-input__append-inner {
display: none !important;
}
>>> .answer-box .v-select__selections {
padding-bottom: 4px !important;
padding-top: 4px !important;
}
.chips {
margin: 0 8px 0 0 !important;
}
.text {
padding-bottom: 0 !important;
}
>>> .trash-icon .v-input__append-outer {
margin-top: 0 !important;
}
.text-field {
padding-bottom: 8px !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
37 changes: 36 additions & 1 deletion src/store/wikis.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,26 @@ 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'] }
]
let captchaQuestions
let questionFromStoreAsArrayOfObjects
if (captchaQuestionsSetting) {
questionFromStoreAsArrayOfObjects = Object.keys(captchaQuestionsSetting).map(
(key) => { return { question: key, answers: captchaQuestionsSetting[key] } }
)
captchaQuestions = questionFromStoreAsArrayOfObjects
} else {
captchaQuestions = JSON.parse(JSON.stringify(defaultQuestions))
}

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

Expand Down Expand Up @@ -90,7 +110,10 @@ const mutations = {
wwWikibaseStringLengthString,
wwWikibaseStringLengthMonolingualText,
wwWikibaseStringLengthMultilang,
wwExtEnableConfirmAccount
wwExtEnableConfirmAccount,
wwUseQuestyCaptcha,
captchaQuestions,
defaultQuestions
}
},
clear_current_wiki_settings (state) {
Expand Down Expand Up @@ -119,6 +142,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 +215,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
Loading