Localisation updates from https://translatewiki.net.
[mediawiki.git] / resources / src / mediawiki.special.block / SpecialBlock.vue
blobd2c0373823af6581782e2a8490ad42e885593a20
1 <template>
2         <!-- @todo Remove after it's no longer new. -->
3         <cdx-message v-if="enableMultiblocks" allow-user-dismiss>
4                 {{ $i18n( 'block-multiblocks-new-feature' ) }}
5         </cdx-message>
6         <cdx-field
7                 class="mw-block-fieldset"
8                 :is-fieldset="true"
9                 :disabled="store.formDisabled"
10         >
11                 <div ref="messagesContainer" class="mw-block-messages">
12                         <cdx-message
13                                 v-if="success"
14                                 type="success"
15                                 :allow-user-dismiss="true"
16                                 class="mw-block-success"
17                         >
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>
21                         </cdx-message>
22                         <cdx-message
23                                 v-for="( formError, index ) in formErrors"
24                                 :key="index"
25                                 type="error"
26                                 class="mw-block-error"
27                                 inline
28                         >
29                                 <!-- eslint-disable-next-line vue/no-v-html -->
30                                 <div v-html="formError"></div>
31                         </cdx-message>
32                 </div>
33                 <user-lookup
34                         v-model="store.targetUser"
35                 ></user-lookup>
37                 <div v-if="showBlockLogs">
38                         <block-log
39                                 :key="`${submitCount}-active`"
40                                 :open="success"
41                                 :can-delete-log-entry="false"
42                                 block-log-type="active"
43                                 @create-block="onCreateBlock"
44                                 @edit-block="onEditBlock"
45                         ></block-log>
46                         <block-log
47                                 :key="`${submitCount}-recent`"
48                                 block-log-type="recent"
49                                 :can-delete-log-entry="canDeleteLogEntry"
50                         ></block-log>
51                         <block-log
52                                 v-if="blockShowSuppressLog"
53                                 :key="`${submitCount}-suppress`"
54                                 block-log-type="suppress"
55                                 :can-delete-log-entry="canDeleteLogEntry"
56                         ></block-log>
58                         <div v-if="showBlockForm" class="mw-block__block-form">
59                                 <block-type-field></block-type-field>
60                                 <expiry-field></expiry-field>
61                                 <reason-field
62                                         v-model:selected="store.reason"
63                                         v-model:other="store.reasonOther"
64                                 ></reason-field>
65                                 <block-details-field></block-details-field>
66                                 <additional-details-field></additional-details-field>
67                                 <confirmation-dialog
68                                         v-if="store.confirmationNeeded"
69                                         v-model:open="confirmationOpen"
70                                         :title="$i18n( 'ipb-confirm' ).text()"
71                                         @confirm="doBlock"
72                                 >
73                                         <template #default>
74                                                 <!-- eslint-disable-next-line vue/no-v-html -->
75                                                 <p v-html="store.confirmationMessage"></p>
76                                         </template>
77                                 </confirmation-dialog>
78                                 <hr class="mw-block-hr">
79                                 <cdx-button
80                                         action="destructive"
81                                         weight="primary"
82                                         class="mw-block-submit"
83                                         @click="onFormSubmission"
84                                 >
85                                         {{ submitButtonMessage }}
86                                 </cdx-button>
87                         </div>
88                 </div>
89         </cdx-field>
90 </template>
92 <script>
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.
108  */
109 module.exports = exports = defineComponent( {
110         name: 'SpecialBlock',
111         components: {
112                 UserLookup,
113                 BlockLog,
114                 BlockTypeField,
115                 ExpiryField,
116                 ReasonField,
117                 BlockDetailsField,
118                 AdditionalDetailsField,
119                 ConfirmationDialog,
120                 CdxButton,
121                 CdxField,
122                 CdxMessage
123         },
124         setup() {
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 )
135                         ) {
136                                 return mw.message( 'block-update' ).text();
137                         }
138                         return mw.message( 'ipbsubmit' ).text();
139                 } );
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;
153                                         scrollToForm();
154                                 } else {
155                                         // If the block ID is invalid, show an error message.
156                                         formErrors.value = [ mw.msg( 'block-invalid-id' ) ];
157                                 }
158                         } );
159                 }
161                 /**
162                  * Show the form for a new block.
163                  */
164                 function onCreateBlock() {
165                         // On initial load, we want the preset values from the URL to be set.
166                         if ( initialLoad ) {
167                                 initialLoad = false;
168                         } else {
169                                 // Subsequent loads should reset the form to the established defaults.
170                                 store.resetForm();
171                         }
172                         formVisible.value = true;
173                         scrollToForm();
174                 }
176                 /**
177                  * Show the form for an existing block.
178                  *
179                  * @param {Object} blockData
180                  */
181                 function onEditBlock( blockData ) {
182                         store.loadFromData( blockData, false );
183                         formVisible.value = true;
184                         scrollToForm();
185                 }
187                 /**
188                  * Animate scrolling to the form.
189                  */
190                 function scrollToForm() {
191                         nextTick( () => {
192                                 document.querySelector( '.mw-block__block-form' ).scrollIntoView( { behavior: 'smooth' } );
193                         } );
194                 }
196                 /**
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.
200                  *
201                  * If the form is valid, send the block request to the server,
202                  * add the success message and scroll to it.
203                  *
204                  * @param {Event} event
205                  */
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;
216                                         return;
217                                 }
218                                 doBlock();
219                         } else {
220                                 // nextTick() needed to ensure error messages are rendered before scrolling.
221                                 nextTick( () => {
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;
229                                 } );
230                         }
231                 }
233                 /**
234                  * Load the block form content from the 'id' URL parameter.
235                  *
236                  * @return {Promise<Object>} A promise that resolves to the block query response.
237                  */
238                 function loadFromIdParam() {
239                         const params = {
240                                 action: 'query',
241                                 list: 'blocks',
242                                 bkids: mw.util.getParamValue( 'id' ),
243                                 formatversion: 2,
244                                 format: 'json',
245                                 bkprop: 'id|user|by|timestamp|expiry|reason|range|flags|restrictions'
246                         };
247                         const api = new mw.Api();
248                         return api.get( params ).then( ( response ) => response.query );
249                 }
251                 /**
252                  * Execute the block request, set the success state and form errors,
253                  * and scroll to the messages container.
254                  */
255                 function doBlock() {
256                         store.doBlock()
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;
262                                         }
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.
267                                         submitCount.value++;
268                                         // Hide the form if the block was successful.
269                                         formVisible.value = false;
270                                 } )
271                                 .fail( ( _, errorObj ) => {
272                                         formErrors.value = [ errorObj.error.info ];
273                                         success.value = false;
274                                 } )
275                                 .always( () => {
276                                         formSubmitted.value = false;
277                                         messagesContainer.value.scrollIntoView( { behavior: 'smooth' } );
278                                 } );
279                 }
281                 return {
282                         store,
283                         messagesContainer,
284                         formErrors,
285                         success,
286                         submitCount,
287                         submitButtonMessage,
288                         enableMultiblocks,
289                         blockShowSuppressLog,
290                         canDeleteLogEntry,
291                         confirmationOpen,
292                         onCreateBlock,
293                         onEditBlock,
294                         onFormSubmission,
295                         doBlock,
296                         showBlockLogs,
297                         showBlockForm
298                 };
299         }
300 } );
301 </script>
303 <style lang="less">
304 @import 'mediawiki.skin.variables.less';
306 // HACK: The accordions within the form need to be full width.
307 .mw-htmlform-codex {
308         max-width: unset;
311 // HACK: Set the max-width of the fields back to what they should be.
312 .cdx-field:not( .mw-block-fieldset ),
313 .mw-block-messages {
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 {
320         margin-top: 0;
323 .mw-block-hideuser .cdx-checkbox__label .cdx-label__label__text {
324         font-weight: @font-weight-bold;
327 .mw-block-hr {
328         margin-top: @spacing-200;
331 .mw-block-submit.cdx-button {
332         margin-top: @spacing-100;
335 .mw-block-error {
336         margin-left: @spacing-75;
339 .mw-block-confirm {
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 {
346         display: none;
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;
357 .mw-block-fieldset {
358         min-width: unset;
360         // Uset font-size until T377902 is resolved.
361         font-size: unset;
363         legend {
364                 font-size: unset;
365         }
367 </style>