2 <!-- @todo Remove after it's no longer new. -->
3 <cdx-message v-if="enableMultiblocks" allow-user-dismiss>
4 {{ $i18n( 'block-multiblocks-new-feature' ) }}
7 class="mw-block-fieldset"
9 :disabled="store.formDisabled"
11 <div ref="messagesContainer" class="mw-block-messages">
15 :allow-user-dismiss="true"
16 class="mw-block-success"
18 <p><strong>{{ $i18n( 'blockipsuccesssub' ) }}</strong></p>
19 <!-- eslint-disable-next-line vue/no-v-html -->
20 <p v-html="$i18n( 'block-success', store.targetUser ).parse()"></p>
23 v-for="( formError, index ) in formErrors"
26 class="mw-block-error"
29 <!-- eslint-disable-next-line vue/no-v-html -->
30 <div v-html="formError"></div>
34 v-model="store.targetUser"
37 <div v-if="showBlockLogs">
39 :key="`${submitCount}-active`"
41 :can-delete-log-entry="false"
42 block-log-type="active"
43 @create-block="onCreateBlock"
44 @edit-block="onEditBlock"
47 :key="`${submitCount}-recent`"
48 block-log-type="recent"
49 :can-delete-log-entry="canDeleteLogEntry"
52 v-if="blockShowSuppressLog"
53 :key="`${submitCount}-suppress`"
54 block-log-type="suppress"
55 :can-delete-log-entry="canDeleteLogEntry"
58 <div v-if="showBlockForm" class="mw-block__block-form">
59 <block-type-field></block-type-field>
60 <expiry-field></expiry-field>
62 v-model:selected="store.reason"
63 v-model:other="store.reasonOther"
65 <block-details-field></block-details-field>
66 <additional-details-field></additional-details-field>
68 v-if="store.confirmationNeeded"
69 v-model:open="confirmationOpen"
70 :title="$i18n( 'ipb-confirm' ).text()"
74 <!-- eslint-disable-next-line vue/no-v-html -->
75 <p v-html="store.confirmationMessage"></p>
77 </confirmation-dialog>
78 <hr class="mw-block-hr">
82 class="mw-block-submit"
83 @click="onFormSubmission"
85 {{ submitButtonMessage }}
93 const { computed, defineComponent, nextTick, ref } = require( 'vue' );
94 const { storeToRefs } = require( 'pinia' );
95 const { CdxButton, CdxField, CdxMessage } = require( '@wikimedia/codex' );
96 const useBlockStore = require( './stores/block.js' );
97 const UserLookup = require( './components/UserLookup.vue' );
98 const BlockLog = require( './components/BlockLog.vue' );
99 const BlockTypeField = require( './components/BlockTypeField.vue' );
100 const ExpiryField = require( './components/ExpiryField.vue' );
101 const ReasonField = require( './components/ReasonField.vue' );
102 const BlockDetailsField = require( './components/BlockDetailsField.vue' );
103 const AdditionalDetailsField = require( './components/AdditionalDetailsField.vue' );
104 const ConfirmationDialog = require( './components/ConfirmationDialog.vue' );
107 * Top-level component for the Special:Block Vue application.
109 module.exports = exports = defineComponent( {
110 name: 'SpecialBlock',
118 AdditionalDetailsField,
125 const store = useBlockStore();
126 const blockShowSuppressLog = mw.config.get( 'blockShowSuppressLog' ) || false;
127 const canDeleteLogEntry = mw.config.get( 'canDeleteLogEntry' ) || false;
128 const { formErrors, formSubmitted, formVisible, success, enableMultiblocks } = storeToRefs( store );
129 const messagesContainer = ref();
130 // Value to use for BlockLog component keys, so they reload after saving.
131 const submitCount = ref( 0 );
132 const submitButtonMessage = computed( () => {
133 if ( ( !store.enableMultiblocks && store.alreadyBlocked ) ||
134 ( store.enableMultiblocks && store.blockId )
136 return mw.message( 'block-update' ).text();
138 return mw.message( 'ipbsubmit' ).text();
140 const confirmationOpen = ref( false );
141 const blockId = computed( () => mw.util.getParamValue( 'id' ) );
142 const showBlockLogs = computed( () => store.targetUser || store.blockId );
143 const showBlockForm = computed( () => formVisible.value || blockId.value );
144 let initialLoad = true;
146 if ( blockId.value ) {
147 loadFromIdParam().then( ( data ) => {
148 if ( data && data.blocks.length ) {
149 // Load the block form content.
150 const block = data.blocks[ 0 ];
151 store.loadFromData( block, true );
152 formVisible.value = true;
155 // If the block ID is invalid, show an error message.
156 formErrors.value = [ mw.msg( 'block-invalid-id' ) ];
162 * Show the form for a new block.
164 function onCreateBlock() {
165 // On initial load, we want the preset values from the URL to be set.
169 // Subsequent loads should reset the form to the established defaults.
172 formVisible.value = true;
177 * Show the form for an existing block.
179 * @param {Object} blockData
181 function onEditBlock( blockData ) {
182 store.loadFromData( blockData, false );
183 formVisible.value = true;
188 * Animate scrolling to the form.
190 function scrollToForm() {
192 document.querySelector( '.mw-block__block-form' ).scrollIntoView( { behavior: 'smooth' } );
197 * Handle form submission. If the form is invalid, show the browser's
198 * validation messages along with any custom validation messages, and
199 * scroll to the first error message to ensure the user sees it.
201 * If the form is valid, send the block request to the server,
202 * add the success message and scroll to it.
204 * @param {Event} event
206 function onFormSubmission( event ) {
207 event.preventDefault();
208 formSubmitted.value = true;
209 success.value = false;
211 // checkValidity() executes browser form validation, which triggers automatic
212 // validation states on applicable components (e.g. fields with `required` attr).
213 if ( event.target.form.checkValidity() && store.expiry ) {
214 if ( store.confirmationNeeded ) {
215 confirmationOpen.value = true;
220 // nextTick() needed to ensure error messages are rendered before scrolling.
222 // Currently, only the expiry field has custom validations.
223 // Scrolling to `cdx-message--error` is merely future-proofing to
224 // ensure the user sees the error message, wherever it may be.
225 // Actual validation logic should live in the respective component.
226 document.querySelector( '.cdx-message--error' )
227 .scrollIntoView( { behavior: 'smooth' } );
228 formSubmitted.value = false;
234 * Load the block form content from the 'id' URL parameter.
236 * @return {Promise<Object>} A promise that resolves to the block query response.
238 function loadFromIdParam() {
242 bkids: mw.util.getParamValue( 'id' ),
245 bkprop: 'id|user|by|timestamp|expiry|reason|range|flags|restrictions'
247 const api = new mw.Api();
248 return api.get( params ).then( ( response ) => response.query );
252 * Execute the block request, set the success state and form errors,
253 * and scroll to the messages container.
257 .done( ( result ) => {
258 // Set the target user to the user that was blocked.
259 // This is primarily for the log entries when blocking a range.
260 if ( result.block && result.block.user ) {
261 store.targetUser = result.block.user;
263 success.value = true;
264 formErrors.value = [];
265 // Bump the submitCount (to re-render the logs) after scrolling
266 // because the log tables may change the length of the page.
268 // Hide the form if the block was successful.
269 formVisible.value = false;
271 .fail( ( _, errorObj ) => {
272 formErrors.value = [ errorObj.error.info ];
273 success.value = false;
276 formSubmitted.value = false;
277 messagesContainer.value.scrollIntoView( { behavior: 'smooth' } );
289 blockShowSuppressLog,
304 @import 'mediawiki.skin.variables.less';
306 // HACK: The accordions within the form need to be full width.
311 // HACK: Set the max-width of the fields back to what they should be.
312 .cdx-field:not( .mw-block-fieldset ),
314 max-width: @size-4000;
317 // HACK: CdxMessage doesn't support v-html, so we need an inner div,
318 // and apply the expected styling to the contents therein.
319 .mw-block-messages .cdx-message__content > div > :first-child {
323 .mw-block-hideuser .cdx-checkbox__label .cdx-label__label__text {
324 font-weight: @font-weight-bold;
328 margin-top: @spacing-200;
331 .mw-block-submit.cdx-button {
332 margin-top: @spacing-100;
336 margin-left: @spacing-75;
340 font-weight: @font-weight-normal;
343 // Hide the log and convenience links showing at the bottom of page.
344 .mw-ipb-conveniencelinks,
345 .mw-warning-with-logexcerpt {
349 // Lower opacity and remove pointer events from accordions while the disabled state is active.
350 // This to prevent users from going to click on something and then having it immediately
351 // disappear, or worse, change to a different link that they click by mistake.
352 .mw-block-fieldset[ disabled ] .cdx-accordion {
353 opacity: @opacity-low;
354 pointer-events: none;
360 // Uset font-size until T377902 is resolved.