From 163fc9945cccc9ee4fda819fd4f6204692470a85 Mon Sep 17 00:00:00 2001 From: Duddino <duddino@duddino.com> Date: Tue, 25 Feb 2025 10:37:28 +0100 Subject: [PATCH 1/8] Add new Form component to Proposal form --- locale/cnr/translation.toml | 9 + locale/de/translation.toml | 9 + locale/en/translation.toml | 9 + locale/es-mx/translation.toml | 9 + locale/fr/translation.toml | 9 + locale/hi/translation.toml | 9 + locale/it/translation.toml | 9 + locale/nl/translation.toml | 9 + locale/ph/translation.toml | 9 + locale/pl/translation.toml | 9 + locale/pt-br/translation.toml | 9 + locale/pt-pt/translation.toml | 9 + locale/template/translation.toml | 9 + locale/uwu/translation.toml | 9 + scripts/form/Form.vue | 48 ++++ scripts/form/Input.vue | 102 +++++++ scripts/form/NumericInput.vue | 38 +++ scripts/governance/ProposalCreateModal.vue | 252 ++++++++++-------- .../governance/ProposalCreateModal.spec.js | 32 ++- 19 files changed, 480 insertions(+), 118 deletions(-) create mode 100644 scripts/form/Form.vue create mode 100644 scripts/form/Input.vue create mode 100644 scripts/form/NumericInput.vue diff --git a/locale/cnr/translation.toml b/locale/cnr/translation.toml index 561a216e6..020374ba1 100644 --- a/locale/cnr/translation.toml +++ b/locale/cnr/translation.toml @@ -211,6 +211,15 @@ creatingShieldTransaction = "" # Creating SHIELD transaction... deleteProposal = "" # Delete Proposal deleteProposalBody = "" # Are you sure you want to delete your proposal? Your collateral will be permanently lost. immatureRewards = "" # Part of your staked balance may be locked for 100 blocks as your reward matures. +formValidationMaxLength = "" # Maximum length allowed: {length} +formValidationMinLength = "" # Minimum length allowed: {length} +formValidationOther = "" # Form validation error +formValidationString = "" # The string contains invalid characters +formValidationUrl = "" # The URL is invalid +formValidationAddress = "" # The address provided is invalid +formValidationMin = "" # The minimum value is: {value} +formValidationMax = "" # The maximum value is: {value} +formValidationInvalidNumber = "" # Invalid Number [ALERTS] INTERNAL_ERROR = "Interna greška, molimo pokušajte ponovo kasnije" # Internal error, please try again later diff --git a/locale/de/translation.toml b/locale/de/translation.toml index 44407ab80..50640e862 100644 --- a/locale/de/translation.toml +++ b/locale/de/translation.toml @@ -211,6 +211,15 @@ creatingShieldTransaction = "" # Creating SHIELD transaction... deleteProposal = "" # Delete Proposal deleteProposalBody = "" # Are you sure you want to delete your proposal? Your collateral will be permanently lost. immatureRewards = "" # Part of your staked balance may be locked for 100 blocks as your reward matures. +formValidationMaxLength = "" # Maximum length allowed: {length} +formValidationMinLength = "" # Minimum length allowed: {length} +formValidationOther = "" # Form validation error +formValidationString = "" # The string contains invalid characters +formValidationUrl = "" # The URL is invalid +formValidationAddress = "" # The address provided is invalid +formValidationMin = "" # The minimum value is: {value} +formValidationMax = "" # The maximum value is: {value} +formValidationInvalidNumber = "" # Invalid Number [ALERTS] INTERNAL_ERROR = "Interner Fehler, bitte versuche es später erneut" # Internal error, please try again later diff --git a/locale/en/translation.toml b/locale/en/translation.toml index 33a01e76c..d757e354e 100644 --- a/locale/en/translation.toml +++ b/locale/en/translation.toml @@ -211,6 +211,15 @@ creatingShieldTransaction = "Creating SHIELD transaction..." # Creating SHIELD t deleteProposal = "Delete Proposal" # Delete Proposal deleteProposalBody = "Are you sure you want to delete your proposal? Your collateral will be permanently lost." # Are you sure you want to delete your proposal? Your collateral will be permanently lost. immatureRewards = "Part of your staked balance may be locked for 100 blocks as your reward matures." # Part of your staked balance may be locked for 100 blocks as your reward matures. +formValidationMaxLength = "Maximum length allowed: {length}" # Maximum length allowed: {length} +formValidationMinLength = "Minimum length allowed: {length}" # Minimum length allowed: {length} +formValidationOther = "Form validation error" # Form validation error +formValidationString = "The string contains invalid characters" # The string contains invalid characters +formValidationUrl = "The URL is invalid" # The URL is invalid +formValidationAddress = "The address provided is invalid" # The address provided is invalid +formValidationMin = "The minimum value is: {value}" # The minimum value is: {value} +formValidationMax = "The maximum value is: {value}" # The maximum value is: {value} +formValidationInvalidNumber = "Invalid Number" # Invalid Number [ALERTS] INTERNAL_ERROR = "Internal error, please try again later" # Internal error, please try again later diff --git a/locale/es-mx/translation.toml b/locale/es-mx/translation.toml index 3b0f178f4..b68e61c6c 100644 --- a/locale/es-mx/translation.toml +++ b/locale/es-mx/translation.toml @@ -211,6 +211,15 @@ creatingShieldTransaction = "" # Creating SHIELD transaction... deleteProposal = "" # Delete Proposal deleteProposalBody = "" # Are you sure you want to delete your proposal? Your collateral will be permanently lost. immatureRewards = "" # Part of your staked balance may be locked for 100 blocks as your reward matures. +formValidationMaxLength = "" # Maximum length allowed: {length} +formValidationMinLength = "" # Minimum length allowed: {length} +formValidationOther = "" # Form validation error +formValidationString = "" # The string contains invalid characters +formValidationUrl = "" # The URL is invalid +formValidationAddress = "" # The address provided is invalid +formValidationMin = "" # The minimum value is: {value} +formValidationMax = "" # The maximum value is: {value} +formValidationInvalidNumber = "" # Invalid Number [ALERTS] INTERNAL_ERROR = "Error interno, vuelve a intentarlo más tarde" # Internal error, please try again later diff --git a/locale/fr/translation.toml b/locale/fr/translation.toml index e39ee4690..c51d05c44 100644 --- a/locale/fr/translation.toml +++ b/locale/fr/translation.toml @@ -211,6 +211,15 @@ creatingShieldTransaction = "" # Creating SHIELD transaction... deleteProposal = "" # Delete Proposal deleteProposalBody = "" # Are you sure you want to delete your proposal? Your collateral will be permanently lost. immatureRewards = "" # Part of your staked balance may be locked for 100 blocks as your reward matures. +formValidationMaxLength = "" # Maximum length allowed: {length} +formValidationMinLength = "" # Minimum length allowed: {length} +formValidationOther = "" # Form validation error +formValidationString = "" # The string contains invalid characters +formValidationUrl = "" # The URL is invalid +formValidationAddress = "" # The address provided is invalid +formValidationMin = "" # The minimum value is: {value} +formValidationMax = "" # The maximum value is: {value} +formValidationInvalidNumber = "" # Invalid Number [ALERTS] INTERNAL_ERROR = "Erreur interne, veuillez réessayer plus tard" # Internal error, please try again later diff --git a/locale/hi/translation.toml b/locale/hi/translation.toml index 31eb6b829..762f7eb52 100644 --- a/locale/hi/translation.toml +++ b/locale/hi/translation.toml @@ -211,6 +211,15 @@ creatingShieldTransaction = "क्रिएटिंग शील्ड ट् deleteProposal = "" # Delete Proposal deleteProposalBody = "" # Are you sure you want to delete your proposal? Your collateral will be permanently lost. immatureRewards = "" # Part of your staked balance may be locked for 100 blocks as your reward matures. +formValidationMaxLength = "" # Maximum length allowed: {length} +formValidationMinLength = "" # Minimum length allowed: {length} +formValidationOther = "" # Form validation error +formValidationString = "" # The string contains invalid characters +formValidationUrl = "" # The URL is invalid +formValidationAddress = "" # The address provided is invalid +formValidationMin = "" # The minimum value is: {value} +formValidationMax = "" # The maximum value is: {value} +formValidationInvalidNumber = "" # Invalid Number [ALERTS] INTERNAL_ERROR = "आंतरिक त्रुटि, कृपया कुछ समय बाद पुनः प्रयास करें" # Internal error, please try again later diff --git a/locale/it/translation.toml b/locale/it/translation.toml index fa2fca9d5..72a29c519 100644 --- a/locale/it/translation.toml +++ b/locale/it/translation.toml @@ -211,6 +211,15 @@ creatingShieldTransaction = "" # Creating SHIELD transaction... deleteProposal = "" # Delete Proposal deleteProposalBody = "" # Are you sure you want to delete your proposal? Your collateral will be permanently lost. immatureRewards = "" # Part of your staked balance may be locked for 100 blocks as your reward matures. +formValidationMaxLength = "" # Maximum length allowed: {length} +formValidationMinLength = "" # Minimum length allowed: {length} +formValidationOther = "" # Form validation error +formValidationString = "" # The string contains invalid characters +formValidationUrl = "" # The URL is invalid +formValidationAddress = "" # The address provided is invalid +formValidationMin = "" # The minimum value is: {value} +formValidationMax = "" # The maximum value is: {value} +formValidationInvalidNumber = "" # Invalid Number [ALERTS] INTERNAL_ERROR = "Errore interno, rirova più tardi" # Internal error, please try again later diff --git a/locale/nl/translation.toml b/locale/nl/translation.toml index 48084ec04..3a44635ef 100644 --- a/locale/nl/translation.toml +++ b/locale/nl/translation.toml @@ -211,6 +211,15 @@ creatingShieldTransaction = "" # Creating SHIELD transaction... deleteProposal = "" # Delete Proposal deleteProposalBody = "" # Are you sure you want to delete your proposal? Your collateral will be permanently lost. immatureRewards = "" # Part of your staked balance may be locked for 100 blocks as your reward matures. +formValidationMaxLength = "" # Maximum length allowed: {length} +formValidationMinLength = "" # Minimum length allowed: {length} +formValidationOther = "" # Form validation error +formValidationString = "" # The string contains invalid characters +formValidationUrl = "" # The URL is invalid +formValidationAddress = "" # The address provided is invalid +formValidationMin = "" # The minimum value is: {value} +formValidationMax = "" # The maximum value is: {value} +formValidationInvalidNumber = "" # Invalid Number [ALERTS] INTERNAL_ERROR = "Interne fout, probeer het later opnieuw" # Internal error, please try again later diff --git a/locale/ph/translation.toml b/locale/ph/translation.toml index b317d4b1d..4189e7fcc 100644 --- a/locale/ph/translation.toml +++ b/locale/ph/translation.toml @@ -211,6 +211,15 @@ creatingShieldTransaction = "" # Creating SHIELD transaction... deleteProposal = "" # Delete Proposal deleteProposalBody = "" # Are you sure you want to delete your proposal? Your collateral will be permanently lost. immatureRewards = "" # Part of your staked balance may be locked for 100 blocks as your reward matures. +formValidationMaxLength = "" # Maximum length allowed: {length} +formValidationMinLength = "" # Minimum length allowed: {length} +formValidationOther = "" # Form validation error +formValidationString = "" # The string contains invalid characters +formValidationUrl = "" # The URL is invalid +formValidationAddress = "" # The address provided is invalid +formValidationMin = "" # The minimum value is: {value} +formValidationMax = "" # The maximum value is: {value} +formValidationInvalidNumber = "" # Invalid Number [ALERTS] INTERNAL_ERROR = "Internal error, Pakiusap uliting muli" # Internal error, please try again later diff --git a/locale/pl/translation.toml b/locale/pl/translation.toml index cb7aaff4c..7bb85efef 100644 --- a/locale/pl/translation.toml +++ b/locale/pl/translation.toml @@ -211,6 +211,15 @@ creatingShieldTransaction = "" # Creating SHIELD transaction... deleteProposal = "" # Delete Proposal deleteProposalBody = "" # Are you sure you want to delete your proposal? Your collateral will be permanently lost. immatureRewards = "" # Part of your staked balance may be locked for 100 blocks as your reward matures. +formValidationMaxLength = "" # Maximum length allowed: {length} +formValidationMinLength = "" # Minimum length allowed: {length} +formValidationOther = "" # Form validation error +formValidationString = "" # The string contains invalid characters +formValidationUrl = "" # The URL is invalid +formValidationAddress = "" # The address provided is invalid +formValidationMin = "" # The minimum value is: {value} +formValidationMax = "" # The maximum value is: {value} +formValidationInvalidNumber = "" # Invalid Number [ALERTS] INTERNAL_ERROR = "Błąd wewnętrzny, spróbuj ponownie później" # Internal error, please try again later diff --git a/locale/pt-br/translation.toml b/locale/pt-br/translation.toml index b5a31527e..28cf35c7e 100644 --- a/locale/pt-br/translation.toml +++ b/locale/pt-br/translation.toml @@ -87,6 +87,15 @@ creatingShieldTransaction = "" # Creating SHIELD transaction... deleteProposal = "" # Delete Proposal deleteProposalBody = "" # Are you sure you want to delete your proposal? Your collateral will be permanently lost. immatureRewards = "" # Part of your staked balance may be locked for 100 blocks as your reward matures. +formValidationMaxLength = "" # Maximum length allowed: {length} +formValidationMinLength = "" # Minimum length allowed: {length} +formValidationOther = "" # Form validation error +formValidationString = "" # The string contains invalid characters +formValidationUrl = "" # The URL is invalid +formValidationAddress = "" # The address provided is invalid +formValidationMin = "" # The minimum value is: {value} +formValidationMax = "" # The maximum value is: {value} +formValidationInvalidNumber = "" # Invalid Number [ALERTS] STAKE_ADDR_SET = "<b>Endereço de Cold Staking definido!</b><br>Ao fazer Stake no futuro este endereço irá ser usado." # <b>Cold Address set!</b><br>Future stakes will use this address. diff --git a/locale/pt-pt/translation.toml b/locale/pt-pt/translation.toml index aa5fc1df1..21ede79bd 100644 --- a/locale/pt-pt/translation.toml +++ b/locale/pt-pt/translation.toml @@ -87,6 +87,15 @@ creatingShieldTransaction = "" # Creating SHIELD transaction... deleteProposal = "" # Delete Proposal deleteProposalBody = "" # Are you sure you want to delete your proposal? Your collateral will be permanently lost. immatureRewards = "" # Part of your staked balance may be locked for 100 blocks as your reward matures. +formValidationMaxLength = "" # Maximum length allowed: {length} +formValidationMinLength = "" # Minimum length allowed: {length} +formValidationOther = "" # Form validation error +formValidationString = "" # The string contains invalid characters +formValidationUrl = "" # The URL is invalid +formValidationAddress = "" # The address provided is invalid +formValidationMin = "" # The minimum value is: {value} +formValidationMax = "" # The maximum value is: {value} +formValidationInvalidNumber = "" # Invalid Number [ALERTS] STAKE_ADDR_SET = "<b>Endereço de Cold Staking definido!</b><br>Ao fazer Stake no futuro irá ser usado este endereço." # <b>Cold Address set!</b><br>Future stakes will use this address. diff --git a/locale/template/translation.toml b/locale/template/translation.toml index c3fc3ebf1..43dc12e07 100644 --- a/locale/template/translation.toml +++ b/locale/template/translation.toml @@ -235,6 +235,15 @@ creatingShieldTransaction = "Creating SHIELD transaction..." deleteProposal = "Delete Proposal" deleteProposalBody = "Are you sure you want to delete your proposal? Your collateral will be permanently lost." immatureRewards = "Part of your staked balance may be locked for 100 blocks as your reward matures." +formValidationMaxLength = "Maximum length allowed: {length}" +formValidationMinLength = "Minimum length allowed: {length}" +formValidationMin = "The minimum value is: {value}" +formValidationMax = "The maximum value is: {value}" +formValidationOther = "Form validation error" +formValidationString = "The string contains invalid characters" +formValidationUrl = "The URL is invalid" +formValidationAddress = "The address provided is invalid" +formValidationInvalidNumber = "Invalid Number" [ALERTS] INTERNAL_ERROR = "Internal error, please try again later" diff --git a/locale/uwu/translation.toml b/locale/uwu/translation.toml index d37966328..1b13f31f4 100644 --- a/locale/uwu/translation.toml +++ b/locale/uwu/translation.toml @@ -211,6 +211,15 @@ creatingShieldTransaction = "" # Creating SHIELD transaction... deleteProposal = "" # Delete Proposal deleteProposalBody = "" # Are you sure you want to delete your proposal? Your collateral will be permanently lost. immatureRewards = "" # Part of your staked balance may be locked for 100 blocks as your reward matures. +formValidationMaxLength = "" # Maximum length allowed: {length} +formValidationMinLength = "" # Minimum length allowed: {length} +formValidationOther = "" # Form validation error +formValidationString = "" # The string contains invalid characters +formValidationUrl = "" # The URL is invalid +formValidationAddress = "" # The address provided is invalid +formValidationMin = "" # The minimum value is: {value} +formValidationMax = "" # The maximum value is: {value} +formValidationInvalidNumber = "" # Invalid Number [ALERTS] INTERNAL_ERROR = "Internal error, pwease try again later" # Internal error, please try again later diff --git a/scripts/form/Form.vue b/scripts/form/Form.vue new file mode 100644 index 000000000..e404dc26a --- /dev/null +++ b/scripts/form/Form.vue @@ -0,0 +1,48 @@ +<script setup> +import { provide, reactive, ref } from 'vue'; +import { translation } from '../i18n.js'; + +const props = defineProps({ + validationFunction: Function, +}); +const formData = reactive({}); +const error = ref(''); +provide('formData', formData); + +const emit = defineEmits(['submit']); +const submitForm = () => { + const res = {}; + error.value = ''; + for (const key in formData) { + // true is the only success + // A string rapresents are error, and is relayed to the user + if (formData[key].validationFunction() !== true) return; + res[key] = formData[key].value; + } + if (props.validationFunction) { + const validation = props.validationFunction(res); + if (validation !== true) { + error.value = validation; + return; + } + } + emit('submit', res); +}; +</script> + +<template> + <form @submit.prevent="submitForm"> + {{ error }} + <slot> </slot> + <slot name="button" :onSubmit="() => submitForm()"> + <button + type="button" + class="pivx-button-big" + style="float: right" + @click="submitForm()" + > + {{ translation.popupConfirm }} + </button> + </slot> + </form> +</template> diff --git a/scripts/form/Input.vue b/scripts/form/Input.vue new file mode 100644 index 000000000..a33de1fb8 --- /dev/null +++ b/scripts/form/Input.vue @@ -0,0 +1,102 @@ +<script setup> +import { ref, inject, watch, toRaw } from 'vue'; +import { tr, translation } from '../i18n.js'; + +const props = defineProps({ + name: String, + validationFunction: Function, + maxLength: Number, + minLength: Number, + placeholder: String, + type: String, + max: Number, + min: Number, + disabled: Boolean, + 'data-testid': String, +}); + +const value = ref(''); +const error = ref(''); +const validationFunction = () => { + if (props.maxLength && value.value.length > props.maxLength) + return tr(translation.formValidationMaxLength, [ + { length: props.maxLength }, + ]); + if (props.minLength && value.value.length < props.minLength) + return tr(translation.formValidationMinLength, [ + { length: props.minLength }, + ]); + if (props.validationFunction) return props.validationFunction(value.value); + return true; +}; +/** + * @type{import('vue').Reactive<{[key: string]: string}>} + */ +const formData = inject('formData'); +watch( + [ + value, + () => props.validationFunction, + () => props.name, + () => props.disabled, + ], + () => { + if (!props.name) return; + if (props.disabled) { + delete formData[props.name]; + return; + } + error.value = ''; + + formData[props.name] = { + value: value.value, + validationFunction: () => { + const res = validationFunction(); + if (res !== true) error.value = res; + return res; + }, + }; + }, + { immediate: true } +); + +watch( + () => props.disabled, + () => { + if (props.disabled) { + value.value = ''; + error.value = ''; + } + } +); +watch( + () => props['data-testid'], + () => console.log(props['data-testid']) +); +</script> + +<template> + <input + :class="{ 'input-error': error?.length }" + v-model="value" + :placeholder="placeholder" + :type="props.type" + :max="props.max" + :min="props.min" + :disabled="props.disabled" + :data-testid="props['dataTestid']" + /> + <div class="validation-summary"> + {{ error }} + </div> +</template> + +<style> +.input-error { + border: 2px solid #ff1d1d; + margin-bottom: 0; +} +.validation-summary { + color: #ff1d1d; +} +</style> diff --git a/scripts/form/NumericInput.vue b/scripts/form/NumericInput.vue new file mode 100644 index 000000000..baf6f3544 --- /dev/null +++ b/scripts/form/NumericInput.vue @@ -0,0 +1,38 @@ +<script setup> +import { tr, translation } from '../i18n.js'; +import Input from './Input.vue'; + +const props = defineProps([ + 'name', + 'validationFunction', + 'max', + 'min', + 'placeholder', + 'dataTestid', +]); + +const validationFunction = (value) => { + if (!Number.isInteger(value)) + return translation.formValidationInvalidNumber; + if (props.max && value > props.max) + return tr(translation.formValidationMax, [{ value: props.max }]); + if (props.min && value < props.min) + return tr(translation.formValidationMin, [{ value: props.min }]); + if (props.validationFunction) return props.validationFunction(value); + return true; +}; +</script> + +<template> + {{ error }} + <Input + type="number" + v-model="value" + :name="name" + :placeholder="placeholder" + :max="props.max" + :min="props.min" + :validation-function="validationFunction" + :data-testid="props.dataTestid" + /> +</template> diff --git a/scripts/governance/ProposalCreateModal.vue b/scripts/governance/ProposalCreateModal.vue index 3b7a5fa17..70790954d 100644 --- a/scripts/governance/ProposalCreateModal.vue +++ b/scripts/governance/ProposalCreateModal.vue @@ -1,30 +1,31 @@ <script setup> import Modal from '../Modal.vue'; +import Form from '../form/Form.vue'; +import Input from '../form/Input.vue'; +import NumericInput from '../form/NumericInput.vue'; import { translation } from '../i18n.js'; import { COIN, cChainParams } from '../chain_params'; -import { toRefs, ref, watch } from 'vue'; +import { toRefs } from 'vue'; +import { isStandardAddress } from '../misc'; const props = defineProps({ advancedMode: Boolean, + isTest: Boolean, }); -const name = ref(''); -const url = ref(''); -const payments = ref(0); -const monthlyPayment = ref(''); -const address = ref(''); const { advancedMode } = toRefs(props); const emit = defineEmits(['close', 'create']); -function submit() { +function submit(data) { emit( 'create', - name.value, - url.value, - payments.value, - monthlyPayment.value, - address.value + data.proposalTitle, + data.proposalUrl, + data.proposalCycles, + data.proposalPayment, + data.proposalAddress ); } -watch(advancedMode, () => (address.value = '')); + +const isSafeStr = /^[a-z0-9 .,;\-_/:?@()]+$/i; </script> <template> @@ -46,88 +47,78 @@ watch(advancedMode, () => (address.value = '')); > </template> <template #body> - <div style="padding-left: 10px; padding-right: 10px"> - <p - style=" - margin-bottom: 12px; - color: #af9cc6; - font-size: 1rem; - font-weight: 500; - " - > - Proposal name - </p> - <input - data-testid="proposalTitle" - maxlength="20" - :placeholder="translation.popupProposalName" - style="text-align: start; margin-bottom: 25px" - v-model="name" - /><br /> - - <p - style=" - margin-bottom: 12px; - color: #af9cc6; - font-size: 1rem; - font-weight: 500; - " - > - URL - </p> - <input - data-testid="proposalUrl" - maxlength="64" - placeholder="https://forum.pivx.org/..." - style="margin-bottom: 25px; text-align: start" - v-model="url" - /><br /> - - <p - style=" - margin-bottom: 12px; - color: #af9cc6; - font-size: 1rem; - font-weight: 500; - " - > - Duration in cycles - </p> - <input - type="number" - data-testid="proposalCycles" - min="1" - :max="cChainParams.current.maxPaymentCycles" - :placeholder="translation.popupProposalDuration" - style="margin-bottom: 25px; text-align: start" - v-model="payments" - /><br /> + <Form @submit="submit"> + <template #default> + <p + style=" + margin-bottom: 12px; + color: #af9cc6; + font-size: 1rem; + font-weight: 500; + " + > + Proposal name + </p> + <Input + name="proposalTitle" + data-testid="proposalTitle" + :max-length="20" + :placeholder="translation.popupProposalName" + :validation-function=" + (value) => { + if (!isSafeStr.test(value)) + return translation.formValidationString; + return true; + } + " + /> + <p + style=" + margin-bottom: 12px; + color: #af9cc6; + font-size: 1rem; + font-weight: 500; + " + > + URL + </p> + <Input + name="proposalUrl" + data-testid="proposalUrl" + :max-length="64" + placeholder="https://forum.pivx.org/..." + :validation-function=" + (value) => { + if (!isSafeStr.test(value)) + return translation.formValidationString; + if ( + !/^(https?):\/\/[^\s/$.?#][^\s]*[^\s/.]\.[^\s/.][^\s]*[^\s.]$/.test( + value + ) + ) + return translation.formValidationUrl; + return true; + } + " + /> + <p + style=" + margin-bottom: 12px; + color: #af9cc6; + font-size: 1rem; + font-weight: 500; + " + > + Duration in cycles + </p> + <NumericInput + name="proposalCycles" + data-testid="proposalCycles" + :min="1" + :max="cChainParams.current.maxPaymentCycles" + :placeholder="translation.popupProposalDuration" + /> - <p - style=" - margin-bottom: 12px; - color: #af9cc6; - font-size: 1rem; - font-weight: 500; - " - > - {{ cChainParams.current.TICKER }} per cycle - </p> - <input - type="number" - data-testid="proposalPayment" - min="10" - :max="cChainParams.current.maxPayment / COIN" - :placeholder=" - cChainParams.current.TICKER + - ' ' + - translation.popupProposalPerCycle - " - style="margin-bottom: 25px; text-align: start" - v-model="monthlyPayment" - /> - <br v-if="!advancedMode" /> - <span v-if="advancedMode"> <p style=" margin-bottom: 12px; @@ -136,19 +127,65 @@ watch(advancedMode, () => (address.value = '')); font-weight: 500; " > - Proposal Address + {{ cChainParams.current.TICKER }} per cycle </p> - <input - data-testid="proposalAddress" - maxlength="34" - :placeholder="translation.popupProposalAddress" - style="margin-bottom: 25px; text-align: start" - v-model="address" + <NumericInput + name="proposalPayment" + data-testid="proposalPayment" + min="10" + :max="cChainParams.current.maxPayment / COIN" + :placeholder="`${cChainParams.current.TICKER} ${translation.popupProposalPerCycle}`" /> - </span> - </div> + <span v-show="advancedMode"> + <p + style=" + margin-bottom: 12px; + color: #af9cc6; + font-size: 1rem; + font-weight: 500; + " + > + Proposal Address + </p> + <Input + name="proposalAddress" + data-testid="proposalAddress" + :validation-function=" + (value) => { + if ( + value.length !== 0 && + !isStandardAddress(value) + ) + return translation.formValidationAddress; + return true; + } + " + :disabled="!advancedMode" + /> + </span> + </template> + + <template #button="slotProps"> + <Teleport + :disabled="props.isTest" + defer + to=".create-proposal-button-container" + > + <button + type="button" + class="pivx-button-big" + style="float: right" + data-testid="proposalSubmit" + @click="slotProps.onSubmit()" + > + {{ translation.popupConfirm }} + </button> + </Teleport> + </template> + </Form> </template> <template #footer> + <div class="create-proposal-button-container"></div> <button type="button" class="pivx-button-big-cancel" @@ -158,15 +195,6 @@ watch(advancedMode, () => (address.value = '')); > {{ translation.popupCancel }} </button> - <button - type="button" - class="pivx-button-big" - style="float: right" - data-testid="proposalSubmit" - @click="submit()" - > - {{ translation.popupConfirm }} - </button> </template> </Modal> </template> diff --git a/tests/components/governance/ProposalCreateModal.spec.js b/tests/components/governance/ProposalCreateModal.spec.js index d820501ca..d5f5ba7fc 100644 --- a/tests/components/governance/ProposalCreateModal.spec.js +++ b/tests/components/governance/ProposalCreateModal.spec.js @@ -1,23 +1,40 @@ import { describe, it, expect, vi } from 'vitest'; import { mount } from '@vue/test-utils'; import ProposalCreateModal from '../../../scripts/governance/ProposalCreateModal.vue'; +import { vi } from 'vitest'; +import { defineComponent, h } from 'vue'; + +vi.stubGlobal( + 'Teleport', + defineComponent({ + render() { + return h('div', this.$slots.default ? this.$slots.default() : []); + }, + }) +); describe('ProposalCreateModal component tests', () => { it('hides address input when advanced mode is false', async () => { const wrapper = mount(ProposalCreateModal, { props: { advancedMode: true }, + global: { + stubs: { + teleport: true, // Stubs out <teleport> completely + }, + }, }); let address = wrapper.find('[data-testid="proposalAddress"]'); // Address input + expect(address.attributes().disabled).toBe(undefined); expect(address.isVisible()).toBe(true); await wrapper.setProps({ advancedMode: false }); address = wrapper.find('[data-testid="proposalAddress"]'); - expect(address.exists()).toBe(false); + expect(address.attributes().disabled).toBe(''); }); it('submits correctly', async () => { const wrapper = mount(ProposalCreateModal, { - props: { advancedMode: true }, + props: { advancedMode: true, isTest: true }, }); const proposalTitle = wrapper.find('[data-testid="proposalTitle"]'); await proposalTitle.setValue('Proposal Title'); @@ -28,17 +45,20 @@ describe('ProposalCreateModal component tests', () => { const proposalPayment = wrapper.find('[data-testid="proposalPayment"]'); await proposalPayment.setValue(20); const address = wrapper.find('[data-testid="proposalAddress"]'); - await address.setValue('DLabSomethingSomething'); - + await address.setValue('DLabsOops'); const proposalSubmit = wrapper.find('[data-testid="proposalSubmit"]'); await proposalSubmit.trigger('click'); + // Nothing should be emitted because address is wrong + expect(wrapper.emitted().create).toBeUndefined(); + await address.setValue('DLabsktzGMnsK5K9uRTMCF6NoYNY6ET4Bb'); + await proposalSubmit.trigger('click'); expect(wrapper.emitted().create).toStrictEqual([ [ 'Proposal Title', 'https://proposal.com/', 3, 20, - 'DLabSomethingSomething', + 'DLabsktzGMnsK5K9uRTMCF6NoYNY6ET4Bb', ], ]); await wrapper.setProps({ advancedMode: false }); @@ -50,7 +70,7 @@ describe('ProposalCreateModal component tests', () => { 'https://proposal.com/', 3, 20, - '', + undefined, ]); }); }); From 03ed84e9c3b0345bb13a95b3b4bffcaf6d9b0e21 Mon Sep 17 00:00:00 2001 From: Duddino <duddino@duddino.com> Date: Mon, 3 Mar 2025 15:00:16 +0100 Subject: [PATCH 2/8] Add proposal confirmation screen --- locale/cnr/translation.toml | 2 + locale/de/translation.toml | 2 + locale/en/translation.toml | 2 + locale/es-mx/translation.toml | 2 + locale/fr/translation.toml | 2 + locale/hi/translation.toml | 2 + locale/it/translation.toml | 2 + locale/nl/translation.toml | 2 + locale/ph/translation.toml | 2 + locale/pl/translation.toml | 2 + locale/pt-br/translation.toml | 2 + locale/pt-pt/translation.toml | 2 + locale/template/translation.toml | 2 + locale/uwu/translation.toml | 2 + scripts/governance/ProposalCreateModal.vue | 99 +++++++++++++++++++++- 15 files changed, 124 insertions(+), 3 deletions(-) diff --git a/locale/cnr/translation.toml b/locale/cnr/translation.toml index 020374ba1..1d1f803dd 100644 --- a/locale/cnr/translation.toml +++ b/locale/cnr/translation.toml @@ -220,6 +220,8 @@ formValidationAddress = "" # The address provided is invalid formValidationMin = "" # The minimum value is: {value} formValidationMax = "" # The maximum value is: {value} formValidationInvalidNumber = "" # Invalid Number +proposalTotal = "" # For a total of +proposalConfirm = "" # Confirm your proposal [ALERTS] INTERNAL_ERROR = "Interna greška, molimo pokušajte ponovo kasnije" # Internal error, please try again later diff --git a/locale/de/translation.toml b/locale/de/translation.toml index 50640e862..248af3448 100644 --- a/locale/de/translation.toml +++ b/locale/de/translation.toml @@ -220,6 +220,8 @@ formValidationAddress = "" # The address provided is invalid formValidationMin = "" # The minimum value is: {value} formValidationMax = "" # The maximum value is: {value} formValidationInvalidNumber = "" # Invalid Number +proposalTotal = "" # For a total of +proposalConfirm = "" # Confirm your proposal [ALERTS] INTERNAL_ERROR = "Interner Fehler, bitte versuche es später erneut" # Internal error, please try again later diff --git a/locale/en/translation.toml b/locale/en/translation.toml index d757e354e..817647307 100644 --- a/locale/en/translation.toml +++ b/locale/en/translation.toml @@ -220,6 +220,8 @@ formValidationAddress = "The address provided is invalid" # The address provided formValidationMin = "The minimum value is: {value}" # The minimum value is: {value} formValidationMax = "The maximum value is: {value}" # The maximum value is: {value} formValidationInvalidNumber = "Invalid Number" # Invalid Number +proposalTotal = "For a total of" # For a total of +proposalConfirm = "Confirm your proposal" # Confirm your proposal [ALERTS] INTERNAL_ERROR = "Internal error, please try again later" # Internal error, please try again later diff --git a/locale/es-mx/translation.toml b/locale/es-mx/translation.toml index b68e61c6c..7aaf2c11a 100644 --- a/locale/es-mx/translation.toml +++ b/locale/es-mx/translation.toml @@ -220,6 +220,8 @@ formValidationAddress = "" # The address provided is invalid formValidationMin = "" # The minimum value is: {value} formValidationMax = "" # The maximum value is: {value} formValidationInvalidNumber = "" # Invalid Number +proposalTotal = "" # For a total of +proposalConfirm = "" # Confirm your proposal [ALERTS] INTERNAL_ERROR = "Error interno, vuelve a intentarlo más tarde" # Internal error, please try again later diff --git a/locale/fr/translation.toml b/locale/fr/translation.toml index c51d05c44..765ad360a 100644 --- a/locale/fr/translation.toml +++ b/locale/fr/translation.toml @@ -220,6 +220,8 @@ formValidationAddress = "" # The address provided is invalid formValidationMin = "" # The minimum value is: {value} formValidationMax = "" # The maximum value is: {value} formValidationInvalidNumber = "" # Invalid Number +proposalTotal = "" # For a total of +proposalConfirm = "" # Confirm your proposal [ALERTS] INTERNAL_ERROR = "Erreur interne, veuillez réessayer plus tard" # Internal error, please try again later diff --git a/locale/hi/translation.toml b/locale/hi/translation.toml index 762f7eb52..4f13df81e 100644 --- a/locale/hi/translation.toml +++ b/locale/hi/translation.toml @@ -220,6 +220,8 @@ formValidationAddress = "" # The address provided is invalid formValidationMin = "" # The minimum value is: {value} formValidationMax = "" # The maximum value is: {value} formValidationInvalidNumber = "" # Invalid Number +proposalTotal = "" # For a total of +proposalConfirm = "" # Confirm your proposal [ALERTS] INTERNAL_ERROR = "आंतरिक त्रुटि, कृपया कुछ समय बाद पुनः प्रयास करें" # Internal error, please try again later diff --git a/locale/it/translation.toml b/locale/it/translation.toml index 72a29c519..16a94583b 100644 --- a/locale/it/translation.toml +++ b/locale/it/translation.toml @@ -220,6 +220,8 @@ formValidationAddress = "" # The address provided is invalid formValidationMin = "" # The minimum value is: {value} formValidationMax = "" # The maximum value is: {value} formValidationInvalidNumber = "" # Invalid Number +proposalTotal = "" # For a total of +proposalConfirm = "" # Confirm your proposal [ALERTS] INTERNAL_ERROR = "Errore interno, rirova più tardi" # Internal error, please try again later diff --git a/locale/nl/translation.toml b/locale/nl/translation.toml index 3a44635ef..c5b7aa8bb 100644 --- a/locale/nl/translation.toml +++ b/locale/nl/translation.toml @@ -220,6 +220,8 @@ formValidationAddress = "" # The address provided is invalid formValidationMin = "" # The minimum value is: {value} formValidationMax = "" # The maximum value is: {value} formValidationInvalidNumber = "" # Invalid Number +proposalTotal = "" # For a total of +proposalConfirm = "" # Confirm your proposal [ALERTS] INTERNAL_ERROR = "Interne fout, probeer het later opnieuw" # Internal error, please try again later diff --git a/locale/ph/translation.toml b/locale/ph/translation.toml index 4189e7fcc..0ce962788 100644 --- a/locale/ph/translation.toml +++ b/locale/ph/translation.toml @@ -220,6 +220,8 @@ formValidationAddress = "" # The address provided is invalid formValidationMin = "" # The minimum value is: {value} formValidationMax = "" # The maximum value is: {value} formValidationInvalidNumber = "" # Invalid Number +proposalTotal = "" # For a total of +proposalConfirm = "" # Confirm your proposal [ALERTS] INTERNAL_ERROR = "Internal error, Pakiusap uliting muli" # Internal error, please try again later diff --git a/locale/pl/translation.toml b/locale/pl/translation.toml index 7bb85efef..c5627e035 100644 --- a/locale/pl/translation.toml +++ b/locale/pl/translation.toml @@ -220,6 +220,8 @@ formValidationAddress = "" # The address provided is invalid formValidationMin = "" # The minimum value is: {value} formValidationMax = "" # The maximum value is: {value} formValidationInvalidNumber = "" # Invalid Number +proposalTotal = "" # For a total of +proposalConfirm = "" # Confirm your proposal [ALERTS] INTERNAL_ERROR = "Błąd wewnętrzny, spróbuj ponownie później" # Internal error, please try again later diff --git a/locale/pt-br/translation.toml b/locale/pt-br/translation.toml index 28cf35c7e..471019353 100644 --- a/locale/pt-br/translation.toml +++ b/locale/pt-br/translation.toml @@ -96,6 +96,8 @@ formValidationAddress = "" # The address provided is invalid formValidationMin = "" # The minimum value is: {value} formValidationMax = "" # The maximum value is: {value} formValidationInvalidNumber = "" # Invalid Number +proposalTotal = "" # For a total of +proposalConfirm = "" # Confirm your proposal [ALERTS] STAKE_ADDR_SET = "<b>Endereço de Cold Staking definido!</b><br>Ao fazer Stake no futuro este endereço irá ser usado." # <b>Cold Address set!</b><br>Future stakes will use this address. diff --git a/locale/pt-pt/translation.toml b/locale/pt-pt/translation.toml index 21ede79bd..db4997831 100644 --- a/locale/pt-pt/translation.toml +++ b/locale/pt-pt/translation.toml @@ -96,6 +96,8 @@ formValidationAddress = "" # The address provided is invalid formValidationMin = "" # The minimum value is: {value} formValidationMax = "" # The maximum value is: {value} formValidationInvalidNumber = "" # Invalid Number +proposalTotal = "" # For a total of +proposalConfirm = "" # Confirm your proposal [ALERTS] STAKE_ADDR_SET = "<b>Endereço de Cold Staking definido!</b><br>Ao fazer Stake no futuro irá ser usado este endereço." # <b>Cold Address set!</b><br>Future stakes will use this address. diff --git a/locale/template/translation.toml b/locale/template/translation.toml index 43dc12e07..3d371f1e6 100644 --- a/locale/template/translation.toml +++ b/locale/template/translation.toml @@ -244,6 +244,8 @@ formValidationString = "The string contains invalid characters" formValidationUrl = "The URL is invalid" formValidationAddress = "The address provided is invalid" formValidationInvalidNumber = "Invalid Number" +proposalTotal = "For a total of" +proposalConfirm = "Confirm your proposal" [ALERTS] INTERNAL_ERROR = "Internal error, please try again later" diff --git a/locale/uwu/translation.toml b/locale/uwu/translation.toml index 1b13f31f4..c5adb1a41 100644 --- a/locale/uwu/translation.toml +++ b/locale/uwu/translation.toml @@ -220,6 +220,8 @@ formValidationAddress = "" # The address provided is invalid formValidationMin = "" # The minimum value is: {value} formValidationMax = "" # The maximum value is: {value} formValidationInvalidNumber = "" # Invalid Number +proposalTotal = "" # For a total of +proposalConfirm = "" # Confirm your proposal [ALERTS] INTERNAL_ERROR = "Internal error, pwease try again later" # Internal error, please try again later diff --git a/scripts/governance/ProposalCreateModal.vue b/scripts/governance/ProposalCreateModal.vue index 70790954d..007ad2960 100644 --- a/scripts/governance/ProposalCreateModal.vue +++ b/scripts/governance/ProposalCreateModal.vue @@ -5,7 +5,7 @@ import Input from '../form/Input.vue'; import NumericInput from '../form/NumericInput.vue'; import { translation } from '../i18n.js'; import { COIN, cChainParams } from '../chain_params'; -import { toRefs } from 'vue'; +import { toRefs, ref, reactive } from 'vue'; import { isStandardAddress } from '../misc'; const props = defineProps({ @@ -14,7 +14,10 @@ const props = defineProps({ }); const { advancedMode } = toRefs(props); const emit = defineEmits(['close', 'create']); -function submit(data) { + const data = reactive({}); +const showConfirmation = ref(false); +function submit() { + showConfirmation.value = false; emit( 'create', data.proposalTitle, @@ -25,6 +28,17 @@ function submit(data) { ); } +function createConfirmationScreen(d) { + data.proposalTitle = d.proposalTitle; + data.proposalUrl = d.proposalUrl; + data.proposalCycles = d.proposalCycles; + data.proposalPayment = d.proposalPayment; + data.proposalAddress = d.proposalAddress; + console.log(d); + console.log(data); + showConfirmation.value = true; +} + const isSafeStr = /^[a-z0-9 .,;\-_/:?@()]+$/i; </script> @@ -47,7 +61,7 @@ const isSafeStr = /^[a-z0-9 .,;\-_/:?@()]+$/i; > </template> <template #body> - <Form @submit="submit"> + <Form @submit="createConfirmationScreen"> <template #default> <p style=" @@ -197,4 +211,83 @@ const isSafeStr = /^[a-z0-9 .,;\-_/:?@()]+$/i; </button> </template> </Modal> + + <Modal :show="showConfirmation"> + <template #header> + <h4>{{ translation.proposalConfirm }}</h4> + </template> + <template #body> + <div class="proposalConfirmContainer"> + <p class="proposalConfirmLabel">Proposal name</p> + <code class="proposalConfirmText">{{ + data.proposalTitle + }}</code> + </div> + <div class="proposalConfirmContainer"> + <p class="proposalConfirmLabel">URL</p> + <code class="proposalConfirmText">{{ data.proposalUrl }}</code> + </div> + <div> + <p class="proposalConfirmLabel">Duration in cycles</p> + <code class="proposalConfirmText">{{ + data.proposalCycles + }}</code> + </div> + + <div class="proposalConfirmContainer"> + <p class="proposalConfirmLabel"> + {{ cChainParams.current.TICKER }} per cycle + </p> + <code class="proposalConfirmText" + >{{ data.proposalPayment }} + </code> + </div> + <div class="proposalConfirmContainer"> + <p class="proposalConfirmLabel"> + {{ translation.proposalTotal }} + </p> + <code class="proposalConfirmText" + >{{ data.proposalPayment * data.proposalCycles }} + </code> + </div> + <div v-if="data.proposalAddress" class="proposalConfirmContainer"> + <p class="proposalConfirmLabel">Proposal Address</p> + <code class="proposalConfirmText" + >{{ data.proposalAddress }} + </code> + </div> + </template> + <template #footer> + <button + type="button" + class="pivx-button-big" + style="float: right" + data-testid="proposalSubmit" + @click="submit()" + > + {{ translation.popupConfirm }} + </button> + + <button + type="button" + class="pivx-button-big-cancel" + style="float: left" + data-testid="proposalCancel" + @click="showConfirmation = false" + > + {{ translation.popupCancel }} + </button> + </template> + </Modal> </template> +<style> +.proposalConfirmLabel { + margin-bottom: 0px; + color: #af9cc6; + font-size: 1rem; + font-weight: 500; +} + .proposalConfirmContainer { + margin-bottom: 10px; + } +</style> From a16c1f5327bf20b5d24d4afd806bebd316262044 Mon Sep 17 00:00:00 2001 From: Duddino <duddino@duddino.com> Date: Mon, 3 Mar 2025 15:21:47 +0100 Subject: [PATCH 3/8] Run prettier --- scripts/governance/ProposalCreateModal.vue | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/governance/ProposalCreateModal.vue b/scripts/governance/ProposalCreateModal.vue index 007ad2960..9a7987bb0 100644 --- a/scripts/governance/ProposalCreateModal.vue +++ b/scripts/governance/ProposalCreateModal.vue @@ -14,10 +14,10 @@ const props = defineProps({ }); const { advancedMode } = toRefs(props); const emit = defineEmits(['close', 'create']); - const data = reactive({}); +const data = reactive({}); const showConfirmation = ref(false); function submit() { - showConfirmation.value = false; + showConfirmation.value = false; emit( 'create', data.proposalTitle, @@ -287,7 +287,7 @@ const isSafeStr = /^[a-z0-9 .,;\-_/:?@()]+$/i; font-size: 1rem; font-weight: 500; } - .proposalConfirmContainer { - margin-bottom: 10px; - } +.proposalConfirmContainer { + margin-bottom: 10px; +} </style> From f1baf23138abe78413e66cef5c4cbd409934aef9 Mon Sep 17 00:00:00 2001 From: Duddino <duddino@duddino.com> Date: Tue, 11 Mar 2025 15:33:10 +0100 Subject: [PATCH 4/8] Fix tests --- scripts/governance/ProposalCreateModal.vue | 10 +++++----- .../components/governance/ProposalCreateModal.spec.js | 11 ++++++++++- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/scripts/governance/ProposalCreateModal.vue b/scripts/governance/ProposalCreateModal.vue index 9a7987bb0..f867a5f97 100644 --- a/scripts/governance/ProposalCreateModal.vue +++ b/scripts/governance/ProposalCreateModal.vue @@ -5,7 +5,7 @@ import Input from '../form/Input.vue'; import NumericInput from '../form/NumericInput.vue'; import { translation } from '../i18n.js'; import { COIN, cChainParams } from '../chain_params'; -import { toRefs, ref, reactive } from 'vue'; +import { toRefs, ref, reactive, watch } from 'vue'; import { isStandardAddress } from '../misc'; const props = defineProps({ @@ -16,15 +16,17 @@ const { advancedMode } = toRefs(props); const emit = defineEmits(['close', 'create']); const data = reactive({}); const showConfirmation = ref(false); + function submit() { showConfirmation.value = false; + emit( 'create', data.proposalTitle, data.proposalUrl, data.proposalCycles, data.proposalPayment, - data.proposalAddress + advancedMode.value ? data.proposalAddress : undefined ); } @@ -34,8 +36,6 @@ function createConfirmationScreen(d) { data.proposalCycles = d.proposalCycles; data.proposalPayment = d.proposalPayment; data.proposalAddress = d.proposalAddress; - console.log(d); - console.log(data); showConfirmation.value = true; } @@ -262,7 +262,7 @@ const isSafeStr = /^[a-z0-9 .,;\-_/:?@()]+$/i; type="button" class="pivx-button-big" style="float: right" - data-testid="proposalSubmit" + data-testid="proposalConfirmSubmit" @click="submit()" > {{ translation.popupConfirm }} diff --git a/tests/components/governance/ProposalCreateModal.spec.js b/tests/components/governance/ProposalCreateModal.spec.js index d5f5ba7fc..edb582b2e 100644 --- a/tests/components/governance/ProposalCreateModal.spec.js +++ b/tests/components/governance/ProposalCreateModal.spec.js @@ -14,7 +14,7 @@ vi.stubGlobal( ); describe('ProposalCreateModal component tests', () => { - it('hides address input when advanced mode is false', async () => { + it.todo('hides address input when advanced mode is false', async () => { const wrapper = mount(ProposalCreateModal, { props: { advancedMode: true }, global: { @@ -52,6 +52,11 @@ describe('ProposalCreateModal component tests', () => { expect(wrapper.emitted().create).toBeUndefined(); await address.setValue('DLabsktzGMnsK5K9uRTMCF6NoYNY6ET4Bb'); await proposalSubmit.trigger('click'); + // Confirm the submission + await wrapper + .find('[data-testid="proposalConfirmSubmit"]') + .trigger('click'); + expect(wrapper.emitted().create).toStrictEqual([ [ 'Proposal Title', @@ -64,6 +69,10 @@ describe('ProposalCreateModal component tests', () => { await wrapper.setProps({ advancedMode: false }); await proposalSubmit.trigger('click'); + // Confirm the submission + await wrapper + .find('[data-testid="proposalConfirmSubmit"]') + .trigger('click'); // When advanced mode is toggled off, address should reset expect(wrapper.emitted().create.at(-1)).toStrictEqual([ 'Proposal Title', From f2f78526acfa15843da48f1f77b2c87d53df29cc Mon Sep 17 00:00:00 2001 From: BreadJS <83626012+BreadJS@users.noreply.github.com> Date: Wed, 19 Mar 2025 16:49:29 +0100 Subject: [PATCH 5/8] UI Redesign --- scripts/Modal.vue | 4 +- scripts/governance/ProposalCreateModal.vue | 123 ++++++++++++++------- 2 files changed, 88 insertions(+), 39 deletions(-) diff --git a/scripts/Modal.vue b/scripts/Modal.vue index d59c10402..c3d7d2438 100644 --- a/scripts/Modal.vue +++ b/scripts/Modal.vue @@ -2,6 +2,7 @@ const props = defineProps({ show: Boolean, modalClass: String, + centered: Boolean }); </script> @@ -25,7 +26,8 @@ const props = defineProps({ <slot name="header"></slot> </div> <div - class="modal-body center-text" + class="modal-body" + :class="{ 'center-text': !centered }" style="padding-bottom: 8px; overflow: auto" > <slot name="body"></slot> diff --git a/scripts/governance/ProposalCreateModal.vue b/scripts/governance/ProposalCreateModal.vue index f867a5f97..1677481ac 100644 --- a/scripts/governance/ProposalCreateModal.vue +++ b/scripts/governance/ProposalCreateModal.vue @@ -212,50 +212,69 @@ const isSafeStr = /^[a-z0-9 .,;\-_/:?@()]+$/i; </template> </Modal> - <Modal :show="showConfirmation"> + <Modal :show="showConfirmation" :centered="true"> <template #header> <h4>{{ translation.proposalConfirm }}</h4> </template> <template #body> - <div class="proposalConfirmContainer"> - <p class="proposalConfirmLabel">Proposal name</p> - <code class="proposalConfirmText">{{ - data.proposalTitle - }}</code> - </div> - <div class="proposalConfirmContainer"> - <p class="proposalConfirmLabel">URL</p> - <code class="proposalConfirmText">{{ data.proposalUrl }}</code> - </div> - <div> - <p class="proposalConfirmLabel">Duration in cycles</p> - <code class="proposalConfirmText">{{ - data.proposalCycles - }}</code> - </div> + <div class="row"> + <div class="col-6"> + <div class="proposalConfirmContainer"> + <p class="proposalConfirmLabel">Proposal name</p> + <code class="proposalConfirmText">{{ data.proposalTitle }}</code> + </div> + </div> - <div class="proposalConfirmContainer"> - <p class="proposalConfirmLabel"> - {{ cChainParams.current.TICKER }} per cycle - </p> - <code class="proposalConfirmText" - >{{ data.proposalPayment }} - </code> - </div> - <div class="proposalConfirmContainer"> - <p class="proposalConfirmLabel"> - {{ translation.proposalTotal }} - </p> - <code class="proposalConfirmText" - >{{ data.proposalPayment * data.proposalCycles }} - </code> - </div> - <div v-if="data.proposalAddress" class="proposalConfirmContainer"> - <p class="proposalConfirmLabel">Proposal Address</p> - <code class="proposalConfirmText" - >{{ data.proposalAddress }} - </code> + <div class="col-6"> + <div> + <p class="proposalConfirmLabel">Duration in cycles</p> + <code class="proposalConfirmText">{{ + data.proposalCycles + }}</code> + </div> + </div> + + <div class="col-6"> + <div class="proposalConfirmContainer"> + <p class="proposalConfirmLabel"> + {{ cChainParams.current.TICKER }} per cycle + </p> + <code class="proposalConfirmText" + >{{ data.proposalPayment }} {{ cChainParams.current.TICKER }} + </code> + </div> + </div> + <div class="col-6"> + <div class="proposalConfirmContainer"> + <p class="proposalConfirmLabel"> + {{ translation.proposalTotal }} + </p> + <code class="proposalConfirmText" + >{{ data.proposalPayment * data.proposalCycles }} {{ cChainParams.current.TICKER }} + </code> + </div> + </div> + + <div class="col-12"> + <div class="proposalConfirmContainer"> + <p class="proposalConfirmLabel">URL</p> + <div class="proposalConfirmText link"><a :href="data.proposalUrl">{{ data.proposalUrl }}</a></div> + </div> + </div> + + + + <div class="col-12"> + <div v-if="data.proposalAddress" class="proposalConfirmContainer"> + <p class="proposalConfirmLabel">Proposal Address</p> + <code class="proposalConfirmText" + >{{ data.proposalAddress }} + </code> + </div> + </div> </div> + + </template> <template #footer> <button @@ -290,4 +309,32 @@ const isSafeStr = /^[a-z0-9 .,;\-_/:?@()]+$/i; .proposalConfirmContainer { margin-bottom: 10px; } + +.proposalConfirmText { + background-color: #0000003d; + padding: 1px 5px 2px 5px; + border-radius: 5px; +} + +.proposalConfirmText.link { + background-color: #0000003d; + padding: 1px 5px 2px 5px; + border-radius: 5px; + width: fit-content; + word-break: break-all; +} + +.proposalConfirmText.link a { + color:#9221ff; + font-size: 87.5%; + font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace!important; +} + +.proposalConfirmText.link a:hover { + text-decoration:underline!important; +} + +code { + color:#E9DEFF; +} </style> From 71f51e97b618195aa0c2f3c13675d50810189e92 Mon Sep 17 00:00:00 2001 From: Duddino <duddino@duddino.com> Date: Thu, 20 Mar 2025 09:51:32 +0100 Subject: [PATCH 6/8] Run pretteir --- scripts/Modal.vue | 2 +- scripts/governance/ProposalCreateModal.vue | 34 +++++++++++++--------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/scripts/Modal.vue b/scripts/Modal.vue index c3d7d2438..c0355f6c4 100644 --- a/scripts/Modal.vue +++ b/scripts/Modal.vue @@ -2,7 +2,7 @@ const props = defineProps({ show: Boolean, modalClass: String, - centered: Boolean + centered: Boolean, }); </script> diff --git a/scripts/governance/ProposalCreateModal.vue b/scripts/governance/ProposalCreateModal.vue index 1677481ac..f2d0f9e68 100644 --- a/scripts/governance/ProposalCreateModal.vue +++ b/scripts/governance/ProposalCreateModal.vue @@ -221,7 +221,9 @@ const isSafeStr = /^[a-z0-9 .,;\-_/:?@()]+$/i; <div class="col-6"> <div class="proposalConfirmContainer"> <p class="proposalConfirmLabel">Proposal name</p> - <code class="proposalConfirmText">{{ data.proposalTitle }}</code> + <code class="proposalConfirmText">{{ + data.proposalTitle + }}</code> </div> </div> @@ -240,7 +242,8 @@ const isSafeStr = /^[a-z0-9 .,;\-_/:?@()]+$/i; {{ cChainParams.current.TICKER }} per cycle </p> <code class="proposalConfirmText" - >{{ data.proposalPayment }} {{ cChainParams.current.TICKER }} + >{{ data.proposalPayment }} + {{ cChainParams.current.TICKER }} </code> </div> </div> @@ -250,7 +253,8 @@ const isSafeStr = /^[a-z0-9 .,;\-_/:?@()]+$/i; {{ translation.proposalTotal }} </p> <code class="proposalConfirmText" - >{{ data.proposalPayment * data.proposalCycles }} {{ cChainParams.current.TICKER }} + >{{ data.proposalPayment * data.proposalCycles }} + {{ cChainParams.current.TICKER }} </code> </div> </div> @@ -258,14 +262,19 @@ const isSafeStr = /^[a-z0-9 .,;\-_/:?@()]+$/i; <div class="col-12"> <div class="proposalConfirmContainer"> <p class="proposalConfirmLabel">URL</p> - <div class="proposalConfirmText link"><a :href="data.proposalUrl">{{ data.proposalUrl }}</a></div> + <div class="proposalConfirmText link"> + <a :href="data.proposalUrl">{{ + data.proposalUrl + }}</a> + </div> </div> </div> - - <div class="col-12"> - <div v-if="data.proposalAddress" class="proposalConfirmContainer"> + <div + v-if="data.proposalAddress" + class="proposalConfirmContainer" + > <p class="proposalConfirmLabel">Proposal Address</p> <code class="proposalConfirmText" >{{ data.proposalAddress }} @@ -273,8 +282,6 @@ const isSafeStr = /^[a-z0-9 .,;\-_/:?@()]+$/i; </div> </div> </div> - - </template> <template #footer> <button @@ -325,16 +332,17 @@ const isSafeStr = /^[a-z0-9 .,;\-_/:?@()]+$/i; } .proposalConfirmText.link a { - color:#9221ff; + color: #9221ff; font-size: 87.5%; - font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace!important; + font-family: SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', + 'Courier New', monospace !important; } .proposalConfirmText.link a:hover { - text-decoration:underline!important; + text-decoration: underline !important; } code { - color:#E9DEFF; + color: #e9deff; } </style> From 21dac525919116aeea99364bb1ae5bebfb0fae62 Mon Sep 17 00:00:00 2001 From: Duddino <duddino@duddino.com> Date: Thu, 20 Mar 2025 10:20:58 +0100 Subject: [PATCH 7/8] Fix modal always being visible --- scripts/governance/Governance.vue | 4 ++-- scripts/governance/ProposalCreateModal.vue | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/scripts/governance/Governance.vue b/scripts/governance/Governance.vue index 6d7531abb..85ff27da1 100644 --- a/scripts/governance/Governance.vue +++ b/scripts/governance/Governance.vue @@ -20,7 +20,7 @@ import { useMasternode } from '../composables/use_masternode'; import { useAlerts } from '../composables/use_alerts.js'; const { createAlert } = useAlerts(); -const showCreateProposalModal = ref(true); +const showCreateProposalModal = ref(false); const wallet = useWallet(); const settings = useSettings(); @@ -236,7 +236,7 @@ async function vote(proposal, voteCode) { <template> <ProposalCreateModal - v-show="showCreateProposalModal" + :show="showCreateProposalModal" :advancedMode="advancedMode" @close="showCreateProposalModal = false" @create="createProposal" diff --git a/scripts/governance/ProposalCreateModal.vue b/scripts/governance/ProposalCreateModal.vue index f2d0f9e68..118063e6e 100644 --- a/scripts/governance/ProposalCreateModal.vue +++ b/scripts/governance/ProposalCreateModal.vue @@ -11,8 +11,9 @@ import { isStandardAddress } from '../misc'; const props = defineProps({ advancedMode: Boolean, isTest: Boolean, + show: Boolean, }); -const { advancedMode } = toRefs(props); +const { advancedMode, show } = toRefs(props); const emit = defineEmits(['close', 'create']); const data = reactive({}); const showConfirmation = ref(false); @@ -43,7 +44,7 @@ const isSafeStr = /^[a-z0-9 .,;\-_/:?@()]+$/i; </script> <template> - <Modal :show="true"> + <Modal :show="show"> <template #header> <h4>{{ translation.popupCreateProposal }}</h4> <span From 9a24b3df048a5daf9900ac59cccb57d5c27bb67e Mon Sep 17 00:00:00 2001 From: Duddino <duddino@duddino.com> Date: Thu, 20 Mar 2025 10:24:38 +0100 Subject: [PATCH 8/8] Fix tests and reenable one --- tests/components/governance/ProposalCreateModal.spec.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/governance/ProposalCreateModal.spec.js b/tests/components/governance/ProposalCreateModal.spec.js index edb582b2e..edf3f46df 100644 --- a/tests/components/governance/ProposalCreateModal.spec.js +++ b/tests/components/governance/ProposalCreateModal.spec.js @@ -14,9 +14,9 @@ vi.stubGlobal( ); describe('ProposalCreateModal component tests', () => { - it.todo('hides address input when advanced mode is false', async () => { + it('hides address input when advanced mode is false', async () => { const wrapper = mount(ProposalCreateModal, { - props: { advancedMode: true }, + props: { advancedMode: true, show: true }, global: { stubs: { teleport: true, // Stubs out <teleport> completely @@ -34,7 +34,7 @@ describe('ProposalCreateModal component tests', () => { it('submits correctly', async () => { const wrapper = mount(ProposalCreateModal, { - props: { advancedMode: true, isTest: true }, + props: { advancedMode: true, isTest: true, show: true }, }); const proposalTitle = wrapper.find('[data-testid="proposalTitle"]'); await proposalTitle.setValue('Proposal Title');