3 :class="`mw-block-log mw-block-log__type-${ blockLogType }`"
4 :open="open || ( blockLogType === 'active' && !formVisible )"
8 <cdx-info-chip :icon="infoChipIcon" :status="infoChipStatus">
14 :columns="!!logEntries.length ? columns : []"
16 :use-row-headers="false"
19 <template v-if="shouldShowAddBlockButton" #header>
24 class="mw-block-log__create-button"
25 @click="$emit( 'create-block' )"
27 <cdx-icon :icon="cdxIconCancel"></cdx-icon>
28 {{ $i18n( 'block-create' ).text() }}
31 <template #empty-state>
34 <template #item-modify="{ item }">
35 <a @click="$emit( 'edit-block', item )">
36 {{ $i18n( 'block-item-edit' ).text() }}
38 {{ $i18n( 'pipe-separator' ).text() }}
40 :href="mw.util.getUrl( 'Special:Unblock/' + targetUser )"
42 {{ $i18n( 'block-item-remove' ).text() }}
45 <template #item-hide="{ item }">
47 :href="mw.util.getUrl( 'Special:RevisionDelete', { type: 'logging', ['ids[' + item + ']']: 1 } )"
49 {{ $i18n( 'block-change-visibility' ).text() }}
52 <template #item-timestamp="{ item }">
55 :href="mw.util.getUrl( 'Special:Log', { logid: item.logid } )"
57 {{ util.formatTimestamp( item.timestamp ) }}
60 v-else-if="item.blockid"
61 :href="mw.util.getUrl( 'Special:BlockList', { wpTarget: `#${ item.blockid }` } )"
63 {{ util.formatTimestamp( item.timestamp ) }}
66 {{ util.formatTimestamp( item.timestamp ) }}
69 <template v-if="blockLogType !== 'active'" #item-type="{ item }">
70 {{ util.getBlockActionMessage( item ) }}
72 <template v-if="blockLogType === 'active'" #item-target="{ item }">
73 <!-- eslint-disable-next-line vue/no-v-html -->
74 <span v-html="$i18n( 'userlink-with-contribs', item ).parse()"></span>
76 <template #item-expiry="{ item }">
77 <span v-if="item.expires || item.duration">
78 {{ util.formatTimestamp( item.expires || item.duration ) }}
82 <template #item-blockedby="{ item }">
83 <!-- eslint-disable-next-line vue/no-v-html -->
84 <span v-html="$i18n( 'userlink-with-contribs', item ).parse()"></span>
86 <template #item-parameters="{ item }">
87 <ul v-if="item && item.length">
88 <li v-for="( parameter, index ) in item" :key="index">
89 {{ util.getBlockFlagMessage( parameter ) }}
94 <template #item-reason="{ item }">
98 <div v-if="moreBlocks" class="mw-block-log-fulllog">
100 :href="mw.util.getUrl( 'Special:Log', { page: 'User:' + targetUser, type: blockLogType === 'suppress' ? 'suppress' : 'block' } )"
102 {{ $i18n( 'log-fulllog' ).text() }}
109 const util = require( '../util.js' );
110 const { computed, defineComponent, ref, watch } = require( 'vue' );
111 const { CdxAccordion, CdxTable, CdxButton, CdxIcon, CdxInfoChip } = require( '@wikimedia/codex' );
112 const { storeToRefs } = require( 'pinia' );
113 const useBlockStore = require( '../stores/block.js' );
114 const { cdxIconCancel, cdxIconClock, cdxIconAlert } = require( '../icons.json' );
116 module.exports = exports = defineComponent( {
118 components: { CdxAccordion, CdxTable, CdxButton, CdxIcon, CdxInfoChip },
138 const store = useBlockStore();
139 const { alreadyBlocked, formVisible, targetUser } = storeToRefs( store );
140 let title = mw.message( 'block-user-previous-blocks' ).text();
141 let emptyState = mw.message( 'block-user-no-previous-blocks' ).text();
142 if ( props.blockLogType === 'active' ) {
143 title = mw.message( 'block-user-active-blocks' ).text();
144 emptyState = mw.message( 'block-user-no-active-blocks' ).text();
145 } else if ( props.blockLogType === 'suppress' ) {
146 title = mw.message( 'block-user-suppressed-blocks' ).text();
147 emptyState = mw.message( 'block-user-no-suppressed-blocks' ).text();
151 ...( props.blockLogType === 'active' || props.canDeleteLogEntry ?
152 [ { id: props.blockLogType === 'active' ? 'modify' : 'hide', label: '', minWidth: '100px' } ] : [] ),
153 { id: 'timestamp', label: mw.message( 'blocklist-timestamp' ).text(), minWidth: '112px' },
154 props.blockLogType === 'recent' || props.blockLogType === 'suppress' ?
155 { id: 'type', label: mw.message( 'blocklist-type-header' ).text(), minWidth: '112px' } :
156 { id: 'target', label: mw.message( 'blocklist-target' ).text(), minWidth: '200px' },
157 { id: 'expiry', label: mw.message( 'blocklist-expiry' ).text(), minWidth: '112px' },
158 { id: 'blockedby', label: mw.message( 'blocklist-by' ).text(), minWidth: '200px' },
159 { id: 'parameters', label: mw.message( 'blocklist-params' ).text(), minWidth: '160px' },
160 { id: 'reason', label: mw.message( 'blocklist-reason' ).text(), minWidth: '160px' }
163 const logEntries = ref( [] );
164 const moreBlocks = ref( false );
165 const FETCH_LIMIT = 10;
167 const logEntriesCount = computed( () => {
168 if ( moreBlocks.value ) {
170 'block-user-label-count-exceeds-limit',
171 mw.language.convertNumber( FETCH_LIMIT )
174 return mw.language.convertNumber( logEntries.value.length );
177 const infoChipIcon = computed( () => props.blockLogType === 'active' ? cdxIconAlert : cdxIconClock );
178 const infoChipStatus = computed( () => logEntries.value.length > 0 && props.blockLogType === 'active' ? 'warning' : 'notice' );
181 * Construct the data object needed for a template row, from a logentry API response.
183 * @param {Array} logevents
186 function logentriesToRows( logevents ) {
188 for ( let i = 0; i < logevents.length; i++ ) {
189 const logevent = logevents[ i ];
192 timestamp: logevent.timestamp,
193 logid: logevent.logid
195 type: logevent.action,
197 expires: logevent.params.expiry,
198 duration: logevent.params.duration,
199 type: logevent.action
201 blockedby: logevent.user,
202 parameters: logevent.params.flags,
203 reason: logevent.comment,
210 watch( targetUser, ( newValue ) => {
212 store.getBlockLogData( props.blockLogType ).then( ( responses ) => {
214 const data = responses[ 0 ].query;
216 if ( props.blockLogType === 'recent' ) {
217 // List of recent block entries.
218 newData = logentriesToRows( data.logevents );
219 moreBlocks.value = newData.length >= FETCH_LIMIT;
221 } else if ( props.blockLogType === 'suppress' ) {
222 // List of suppress/block or suppress/reblock log entries.
223 newData.push( ...logentriesToRows( data.logevents ) );
224 newData.push( ...logentriesToRows( responses[ 1 ].query.logevents ) );
225 newData.sort( ( a, b ) => b.timestamp.logid - a.timestamp.logid );
226 moreBlocks.value = newData.length >= FETCH_LIMIT;
227 // Re-apply limit, as each may have been longer.
228 newData = newData.slice( 0, FETCH_LIMIT );
231 // List of active blocks.
232 for ( let i = 0; i < data.blocks.length; i++ ) {
234 // Store the entire API response, for passing in when editing the block.
235 modify: data.blocks[ i ],
237 timestamp: data.blocks[ i ].timestamp,
238 blockid: data.blocks[ i ].id
240 target: data.blocks[ i ].user,
242 expires: data.blocks[ i ].expiry,
243 duration: mw.util.isInfinity( data.blocks[ i ].expiry ) ? 'infinity' : null
245 blockedby: data.blocks[ i ].by,
248 data.blocks[ i ].anononly ? 'anononly' : null,
249 data.blocks[ i ].nocreate ? 'nocreate' : null,
250 data.blocks[ i ].autoblock ? null : 'noautoblock',
251 data.blocks[ i ].noemail ? 'noemail' : null,
252 data.blocks[ i ].allowusertalk ? null : 'nousertalk',
253 data.blocks[ i ].hidden ? 'hiddenname' : null
254 ].filter( ( e ) => e !== null ),
255 reason: data.blocks[ i ].reason
260 logEntries.value = newData;
263 moreBlocks.value = false;
264 logEntries.value = [];
266 }, { immediate: true } );
268 // Show the 'Add block' button in the active blocks accordion if:
269 // * Multiblocks is enabled
270 // * Multiblocks is disabled and the user is not already blocked
271 const shouldShowAddBlockButton = computed(
272 () => props.blockLogType === 'active' && (
273 store.enableMultiblocks || !alreadyBlocked.value
291 shouldShowAddBlockButton
298 @import 'mediawiki.skin.variables.less';
301 word-break: auto-phrase;
303 // Align the new-block button to the left, because there's no table caption.
305 justify-content: flex-start;
313 .mw-block-log-fulllog {
314 margin-top: @spacing-50;
317 .mw-block-active-blocks__menu {