Localisation updates from https://translatewiki.net.
[mediawiki.git] / resources / src / mediawiki.special.block / components / UserLookup.vue
blobbed219e9cc3f29dbe87b988ec49833e5eab68914
1 <template>
2         <cdx-field
3                 :is-fieldset="true"
4                 :status="status"
5                 :messages="messages"
6         >
7                 <cdx-lookup
8                         v-model:selected="selection"
9                         v-model:input-value="currentSearchTerm"
10                         class="mw-block-target"
11                         name="wpTarget"
12                         required
13                         :clearable="true"
14                         :menu-items="menuItems"
15                         :placeholder="$i18n( 'block-target-placeholder' ).text()"
16                         :start-icon="cdxIconSearch"
17                         @input="onInput"
18                         @change="onChange"
19                         @blur="onChange"
20                         @clear="onClear"
21                         @update:selected="onSelect"
22                 >
23                 </cdx-lookup>
24                 <template #label>
25                         {{ $i18n( 'block-target' ).text() }}
26                 </template>
27                 <div class="mw-block-conveniencelinks">
28                         <span v-if="!!targetUser">
29                                 <a
30                                         :href="mw.util.getUrl( contribsTitle )"
31                                         :title="contribsTitle"
32                                 >
33                                         {{ $i18n( 'ipb-blocklist-contribs', targetUser ) }}
34                                 </a>
35                         </span>
36                 </div>
37         </cdx-field>
38 </template>
40 <script>
41 const { computed, defineComponent, onMounted, ref, watch } = require( 'vue' );
42 const { CdxLookup, CdxField } = require( '@wikimedia/codex' );
43 const { storeToRefs } = require( 'pinia' );
44 const { cdxIconSearch } = require( '../icons.json' );
45 const useBlockStore = require( '../stores/block.js' );
46 const api = new mw.Api();
48 /**
49  * User lookup component for Special:Block.
50  *
51  * @todo Abstract for general use in MediaWiki (T375220)
52  */
53 module.exports = exports = defineComponent( {
54         name: 'UserLookup',
55         components: { CdxLookup, CdxField },
56         props: {
57                 modelValue: { type: [ String, null ], required: true }
58         },
59         emits: [
60                 'update:modelValue'
61         ],
62         setup( props ) {
63                 const store = useBlockStore();
64                 const { targetUser } = storeToRefs( store );
65                 let htmlInput;
67                 onMounted( () => {
68                         // Get the input element.
69                         htmlInput = document.querySelector( 'input[name="wpTarget"]' );
70                         // Focus the input on mount.
71                         htmlInput.focus();
72                 } );
74                 // Set a flag to keep track of pending API requests, so we can abort if
75                 // the target string changes
76                 let pending = false;
78                 // Codex Lookup component requires a v-modeled `selected` prop.
79                 // Until a selection is made, the value may be set to null.
80                 // We instead want to only update the targetUser for non-null values
81                 // (made either via selection, or the 'change' event).
82                 const selection = ref( props.modelValue || '' );
83                 // This is the source of truth for what should be the target user,
84                 // but it should only change on 'change' or 'select' events,
85                 // otherwise we'd fire off API queries for the block log unnecessarily.
86                 const currentSearchTerm = ref( props.modelValue || '' );
87                 const menuItems = ref( [] );
88                 const status = ref( 'default' );
89                 const messages = ref( {} );
91                 watch( targetUser, ( newValue ) => {
92                         if ( newValue ) {
93                                 currentSearchTerm.value = newValue;
94                         }
95                 } );
97                 /**
98                  * Get search results.
99                  *
100                  * @param {string} searchTerm
101                  * @return {Promise}
102                  */
103                 function fetchResults( searchTerm ) {
104                         const params = {
105                                 action: 'query',
106                                 format: 'json',
107                                 formatversion: 2,
108                                 list: 'allusers',
109                                 aulimit: '10',
110                                 auprefix: searchTerm
111                         };
113                         return api.get( params )
114                                 .then( ( response ) => response.query );
115                 }
117                 /**
118                  * Handle lookup input.
119                  *
120                  * @param {string} value
121                  */
122                 function onInput( value ) {
123                         // Abort any existing request if one is still pending
124                         if ( pending ) {
125                                 pending = false;
126                                 api.abort();
127                         }
129                         // Internally track the current search term.
130                         currentSearchTerm.value = value;
132                         // Do nothing if we have no input.
133                         if ( !value ) {
134                                 menuItems.value = [];
135                                 return;
136                         }
138                         fetchResults( value )
139                                 .then( ( data ) => {
140                                         pending = false;
142                                         // Make sure this data is still relevant first.
143                                         if ( currentSearchTerm.value !== value ) {
144                                                 return;
145                                         }
147                                         // Reset the menu items if there are no results.
148                                         if ( !data.allusers || data.allusers.length === 0 ) {
149                                                 menuItems.value = [];
150                                                 return;
151                                         }
153                                         // Build an array of menu items.
154                                         menuItems.value = data.allusers.map( ( result ) => ( {
155                                                 label: result.name,
156                                                 value: result.name
157                                         } ) );
158                                 } )
159                                 .catch( () => {
160                                         // On error, set results to empty.
161                                         menuItems.value = [];
162                                 } );
163                 }
165                 /**
166                  * Validate the input element.
167                  *
168                  * @param {HTMLInputElement} el
169                  */
170                 function validate( el ) {
171                         if ( el.checkValidity() ) {
172                                 status.value = 'default';
173                                 messages.value = {};
174                         } else {
175                                 status.value = 'error';
176                                 messages.value = { error: el.validationMessage };
177                         }
178                 }
180                 /**
181                  * Handle lookup change.
182                  */
183                 function onChange() {
184                         // Use the currentSearchTerm value instead of the event target value,
185                         // since the event can be fired before the watcher updates the value.
186                         setTarget( currentSearchTerm.value );
187                 }
189                 /**
190                  * When the clear button is clicked.
191                  */
192                 function onClear() {
193                         store.resetForm( true );
194                         htmlInput.focus();
195                 }
197                 /**
198                  * Handle lookup selection.
199                  */
200                 function onSelect() {
201                         if ( selection.value !== null ) {
202                                 setTarget( selection.value );
203                         }
204                 }
206                 /**
207                  * Set the target user and trigger validation.
208                  *
209                  * @param {string} value
210                  */
211                 function setTarget( value ) {
212                         validate( htmlInput );
213                         targetUser.value = value;
214                 }
216                 // Change the address bar to reflect the newly-selected target (while keeping all URL parameters).
217                 // Do this when the targetUser changes, which is not necessarily when the CdxLookup selection changes.
218                 watch( () => targetUser.value, () => {
219                         const specialBlockUrl = mw.util.getUrl( 'Special:Block' + ( targetUser.value ? '/' + targetUser.value : '' ) );
220                         if ( window.location.pathname !== specialBlockUrl ) {
221                                 const newUrl = ( new URL( `${ specialBlockUrl }${ window.location.search }`, window.location.origin ) ).toString();
222                                 window.history.replaceState( null, '', newUrl );
223                         }
224                 } );
226                 const contribsTitle = computed( () => `Special:Contributions/${ targetUser.value }` );
228                 return {
229                         mw,
230                         contribsTitle,
231                         targetUser,
232                         menuItems,
233                         onChange,
234                         onInput,
235                         onClear,
236                         onSelect,
237                         cdxIconSearch,
238                         currentSearchTerm,
239                         selection,
240                         status,
241                         messages
242                 };
243         }
244 } );
245 </script>
247 <style lang="less">
248 .mw-block-conveniencelinks {
249         a {
250                 font-size: 90%;
251         }
253 </style>