Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F5970149
SpecialBlock.vue
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
16 KB
Referenced Files
None
Subscribers
None
SpecialBlock.vue
View Options
<template>
<!-- @todo Remove after it's no longer new. -->
<cdx-message v-if="enableMultiblocks" allow-user-dismiss>
{{ $i18n( 'block-multiblocks-new-feature' ) }}
</cdx-message>
<!-- @todo Remove some time after deprecation -->
<cdx-message
v-if="wasRedirected"
allow-user-dismiss
>
{{ $i18n( 'block-unblock-redirected' ) }}
</cdx-message>
<cdx-field
class="mw-block-fieldset"
:is-fieldset="true"
:disabled="store.formDisabled"
>
<div ref="messagesContainer" class="mw-block-messages">
<cdx-message
v-if="blockAdded"
type="success"
:allow-user-dismiss="true"
class="mw-block-success"
>
<p><strong>{{ blockSavedMessage }}</strong></p>
<!-- eslint-disable-next-line vue/no-v-html -->
<p v-html="$i18n( 'block-success', store.targetUser ).parse()"></p>
</cdx-message>
<cdx-message
v-if="blockRemoved"
type="success"
:allow-user-dismiss="true"
>
<p>{{ $i18n( 'block-removed' ) }}</p>
</cdx-message>
<cdx-message
v-for="( formError, index ) in formErrors"
:key="index"
type="error"
inline
>
<!-- eslint-disable-next-line vue/no-v-html -->
<div v-html="formError"></div>
</cdx-message>
</div>
<user-lookup
v-model="store.targetUser"
></user-lookup>
<div v-if="showBlockLogs">
<block-log
:key="`${submitCount}-active`"
:open="blockAdded || blockRemoved"
:can-delete-log-entry="false"
block-log-type="active"
@edit-block="onEditBlock"
@remove-block="onRemoveBlock"
></block-log>
<block-log
v-if="mw.util.isIPAddress( store.targetUser, true )"
:key="`${submitCount}-active-ranges`"
:can-delete-log-entry="false"
block-log-type="active-ranges"
></block-log>
<block-log
:key="`${submitCount}-recent`"
block-log-type="recent"
:can-delete-log-entry="canDeleteLogEntry"
></block-log>
<block-log
v-if="blockShowSuppressLog"
:key="`${submitCount}-suppress`"
block-log-type="suppress"
:can-delete-log-entry="canDeleteLogEntry"
></block-log>
<div
v-if="formVisible"
class="mw-block__block-form"
@change="formDirty = true"
>
<h2>{{ formHeaderText }}</h2>
<block-type-field></block-type-field>
<expiry-field></expiry-field>
<reason-field v-model="store.reason"></reason-field>
<block-details-field></block-details-field>
<additional-details-field></additional-details-field>
<confirmation-dialog
v-if="store.confirmationNeeded"
v-model:open="confirmationOpen"
:title="$i18n( 'ipb-confirm' ).text()"
primary-action-type="destructive"
:primary-action-label=" $i18n( 'block-confirm-yes' ).text()"
:default-action-label=" $i18n( 'block-confirm-no' ).text()"
@confirm="doBlock"
>
<template #default>
<!-- eslint-disable-next-line vue/no-v-html -->
<p v-html="store.confirmationMessage"></p>
</template>
</confirmation-dialog>
<hr class="mw-block-hr">
<cdx-button
action="default"
data-test="cancel-edit-button"
weight="primary"
type="button"
@click="onFormCancel"
>
{{ $i18n( 'block-cancel' ) }}
</cdx-button>
<cdx-button
action="progressive"
weight="primary"
class="mw-block-submit"
@click="onFormSubmission"
>
{{ $i18n( 'block-submit' ).text() }}
</cdx-button>
</div>
<div v-else-if="shouldShowAddBlockButton">
<cdx-button
type="button"
action="progressive"
weight="primary"
class="mw-block__create-button"
@click="onCreateBlock"
>
{{ $i18n( 'block-create' ).text() }}
</cdx-button>
</div>
</div>
</cdx-field>
<confirmation-dialog
v-model:open="removalConfirmationOpen"
:title="$i18n( 'block-removal-title' ).text()"
primary-action-type="progressive"
:primary-action-label=" $i18n( 'block-removal-confirm-yes' ).text()"
:default-action-label=" $i18n( 'block-removal-confirm-no' ).text()"
@confirm="doRemoveBlock"
>
<template #default>
<cdx-field>
<template #label>
{{ $i18n( 'block-reason' ).text() }}
</template>
<cdx-text-input
v-model="store.removalReason"
name="wpRemovalReason"
:placeholder="$i18n( 'block-removal-reason-placeholder' ).text()"
></cdx-text-input>
</cdx-field>
<cdx-field>
<cdx-checkbox
v-model="store.watchUser"
>
{{ $i18n( 'ipbwatchuser' ) }}
</cdx-checkbox>
</cdx-field>
</template>
</confirmation-dialog>
</template>
<script>
const { computed, defineComponent, nextTick, onMounted, ref, watch } = require( 'vue' );
const { storeToRefs } = require( 'pinia' );
const { CdxButton, CdxTextInput, CdxCheckbox, CdxField, CdxMessage } = require( '@wikimedia/codex' );
const useBlockStore = require( './stores/block.js' );
const UserLookup = require( './components/UserLookup.vue' );
const BlockLog = require( './components/BlockLog.vue' );
const BlockTypeField = require( './components/BlockTypeField.vue' );
const ExpiryField = require( './components/ExpiryField.vue' );
const ReasonField = require( './components/ReasonField.vue' );
const BlockDetailsField = require( './components/BlockDetailsField.vue' );
const AdditionalDetailsField = require( './components/AdditionalDetailsField.vue' );
const ConfirmationDialog = require( './components/ConfirmationDialog.vue' );
/**
* Top-level component for the Special:Block Vue application.
*/
module.exports = exports = defineComponent( {
name: 'SpecialBlock',
components: {
UserLookup,
BlockLog,
BlockTypeField,
ExpiryField,
ReasonField,
BlockDetailsField,
AdditionalDetailsField,
ConfirmationDialog,
CdxButton,
CdxTextInput,
CdxCheckbox,
CdxField,
CdxMessage
},
setup() {
const store = useBlockStore();
const blockShowSuppressLog = mw.config.get( 'blockShowSuppressLog' ) || false;
const canDeleteLogEntry = mw.config.get( 'blockCanDeleteLogEntry' ) || false;
const {
alreadyBlocked,
formErrors,
formSubmitted,
formVisible,
formDirty,
blockAdded,
blockRemoved,
enableMultiblocks,
removalConfirmationOpen
} = storeToRefs( store );
const messagesContainer = ref();
const blockSavedMessage = ref( '' );
// Value to use for BlockLog component keys, so they reload after saving.
const submitCount = ref( 0 );
const formHeaderText = computed( () => {
if ( ( !store.enableMultiblocks && store.alreadyBlocked ) ||
( store.enableMultiblocks && store.blockId )
) {
return mw.message( 'block-update' ).text();
}
return mw.message( 'block-create' ).text();
} );
const confirmationOpen = ref( false );
const showBlockLogs = computed( () => ( store.targetUser && store.targetExists ) || store.blockId );
// TODO: Remove some time after deprecation
// T382539: Check if we've been redirected from Special:Unblock
const wasRedirected = mw.util.getParamValue( 'redirected' );
let initialLoad = true;
onMounted( () => {
// Prevent the window from being closed as long as we have the form open
mw.confirmCloseWindow( {
test: () => formVisible.value && formDirty.value
} );
// If we're editing or removing via an id URL parameter, check that the block exists.
if ( store.blockId ) {
loadFromId( store.blockId ).then( ( data ) => {
if ( data && data.blocks.length ) {
if ( mw.util.getParamValue( 'remove' ) === '1' ) {
// Fire the remove click handler manually.
onRemoveBlock( store.blockId );
} else {
// Load the block form content.
const block = data.blocks[ 0 ];
store.loadFromData( block, true );
formVisible.value = true;
scrollToForm();
}
} else {
// If the block ID is invalid, show an error message.
formErrors.value = [ mw.msg( 'block-invalid-id' ) ];
store.blockId = null;
}
} );
}
} );
/**
* Show the form for a new block.
*/
function onCreateBlock() {
// On initial load, we want the preset values from the URL to be set.
if ( initialLoad ) {
initialLoad = false;
} else {
// Subsequent loads should reset the form to the established defaults.
store.resetForm();
}
formVisible.value = true;
scrollToForm();
}
/**
* Show the form for an existing block.
*
* @param {Object} blockData
*/
function onEditBlock( blockData ) {
store.loadFromData( blockData, false );
formVisible.value = true;
scrollToForm();
initialLoad = false;
}
/**
* Click handler for the 'remove' links, to open the removal confirmation dialog.
*
* @param {number} currentBlockId
*/
function onRemoveBlock( currentBlockId ) {
formVisible.value = false;
store.blockId = currentBlockId;
store.removalReason = mw.config.get( 'blockRemovalReasonPreset' ) || '';
store.watchUser = false;
removalConfirmationOpen.value = true;
}
/**
* Handler for the primary action of the removal confirmation dialog.
*/
function doRemoveBlock() {
store.doRemoveBlock()
.then( () => {
removalConfirmationOpen.value = false;
submitCount.value++;
blockRemoved.value = true;
formErrors.value = [];
} )
.fail( ( _, errorObj ) => {
formErrors.value = [ errorObj.error.info ];
} )
.always( () => {
blockAdded.value = false;
formSubmitted.value = false;
} );
}
/**
* Animate scrolling to the form.
*/
function scrollToForm() {
nextTick( () => {
document.querySelector( '.mw-block__block-form' ).scrollIntoView( { behavior: 'smooth' } );
} );
}
/**
* Handle form submission. If the form is invalid, show the browser's
* validation messages along with any custom validation messages, and
* scroll to the first error message to ensure the user sees it.
*
* If the form is valid, send the block request to the server,
* add the success message and scroll to it.
*
* @param {Event} event
*/
function onFormSubmission( event ) {
event.preventDefault();
formSubmitted.value = true;
blockAdded.value = false;
// checkValidity() executes browser form validation, which triggers automatic
// validation states on applicable components (e.g. fields with `required` attr).
if ( event.target.form.checkValidity() && store.expiry ) {
if ( store.confirmationNeeded ) {
confirmationOpen.value = true;
return;
}
doBlock();
} else {
// nextTick() needed to ensure error messages are rendered before scrolling.
nextTick( () => {
// Currently, only the expiry field has custom validations.
// Scrolling to `cdx-message--error` is merely future-proofing to
// ensure the user sees the error message, wherever it may be.
// Actual validation logic should live in the respective component.
const firstError = document.querySelector( '.cdx-message--error' );
if ( firstError ) {
// Guard against there not being any parent fieldset.
const firstErrorFieldset = firstError.closest( 'fieldset' );
( firstErrorFieldset || firstError ).scrollIntoView( { behavior: 'smooth' } );
}
formSubmitted.value = false;
} );
}
}
/**
* Handle form cancel button.
*
* @param {Event} event
*/
function onFormCancel( event ) {
event.preventDefault();
store.resetForm();
formVisible.value = false;
nextTick( () => {
messagesContainer.value.scrollIntoView( { behavior: 'smooth' } );
} );
}
/**
* Load data for a given block.
*
* @param {string} id The block ID to load.
* @return {Promise<Object>} A promise that resolves to the block query response.
*/
function loadFromId( id ) {
const params = {
action: 'query',
list: 'blocks',
bkids: id,
formatversion: 2,
format: 'json',
bkprop: 'id|user|by|timestamp|expiry|reason|range|flags|restrictions'
};
const api = new mw.Api();
return api.get( params ).then( ( response ) => response.query );
}
/**
* Execute the block request, set the success state and form errors,
* and scroll to the messages container.
*/
function doBlock() {
store.doBlock()
.done( ( result ) => {
// Set the target user to the user that was blocked.
// This is primarily for the log entries when blocking a range.
if ( result.block && result.block.user ) {
store.targetUser = result.block.user;
}
// Add the success message.
if ( store.blockId ) {
blockSavedMessage.value = mw.message( 'block-updated-message' ).text();
} else {
blockSavedMessage.value = mw.message( 'block-added-message' ).text();
}
blockAdded.value = true;
formErrors.value = [];
// Bump the submitCount (to re-render the logs) after scrolling
// because the log tables may change the length of the page.
submitCount.value++;
// Hide the form if the block was successful.
formVisible.value = false;
// Reset the form so no block data leaks into the next block (T384822).
store.resetForm( false, false );
// Fire clientside hook for scripts that want to do stuff post-blocking.
// This is documented in init.js since JSDoc doesn't parse Vue files (T360456).
mw.hook( 'SpecialBlock.block' ).fire( result.block );
} )
.fail( ( _, errorObj ) => {
formErrors.value = errorObj.errors.map( ( e ) => e.html );
blockAdded.value = false;
} )
.always( () => {
formSubmitted.value = false;
blockRemoved.value = false;
messagesContainer.value.scrollIntoView( { behavior: 'smooth' } );
} );
}
// We need to reset the form so no block data is set.
watch( removalConfirmationOpen, ( newValue ) => {
if ( !newValue ) {
store.resetForm();
}
} );
watch( formVisible, ( newValue ) => {
// Notify scripts that the form visibility changed. Documented in init.js.
mw.hook( 'SpecialBlock.form' ).fire( newValue, store.targetUser, store.blockId );
// Submit the form if form is visible and 'Enter' is pressed
if ( newValue ) {
nextTick( () => {
const blockForm = document.querySelector( '.mw-block__block-form' );
blockForm.addEventListener( 'keypress', ( event ) => {
if ( event.key === 'Enter' ) {
onFormSubmission( event );
}
} );
} );
}
} );
// Show the 'Add block' button if:
// * the target user exists AND EITHER
// * multiblocks is enabled, OR
// * multiblocks is disabled AND the user is not already blocked
const shouldShowAddBlockButton = computed(
() => store.targetExists && (
store.enableMultiblocks || !alreadyBlocked.value
)
);
return {
store,
messagesContainer,
formErrors,
formDirty,
blockAdded,
blockRemoved,
shouldShowAddBlockButton,
submitCount,
blockSavedMessage,
formHeaderText,
enableMultiblocks,
blockShowSuppressLog,
canDeleteLogEntry,
confirmationOpen,
removalConfirmationOpen,
onCreateBlock,
onEditBlock,
onFormCancel,
onFormSubmission,
doBlock,
onRemoveBlock,
doRemoveBlock,
showBlockLogs,
formVisible,
wasRedirected,
mw
};
}
} );
</script>
<style lang="less">
@import 'mediawiki.skin.variables.less';
// HACK: The accordions within the form need to be full width.
.mw-htmlform-codex {
max-width: unset;
}
// HACK: Set the max-width of the fields back to what they should be.
.cdx-field:not( .mw-block-fieldset ),
.mw-block-messages {
max-width: @size-4000;
}
// HACK: CdxMessage doesn't support v-html, so we need an inner div,
// and apply the expected styling to the contents therein.
.mw-block-messages .cdx-message__content > div {
> :first-child {
margin-top: 0;
}
> :last-child {
margin-bottom: 0;
}
}
.mw-block-hideuser .cdx-checkbox__label .cdx-label__label__text {
font-weight: @font-weight-bold;
}
.mw-block-hr {
margin-top: @spacing-200;
}
.mw-block-confirm {
font-weight: @font-weight-normal;
}
.cdx-button.mw-block-submit,
.cdx-button.mw-block__create-button {
margin-top: @spacing-100;
}
// Lower opacity and remove pointer events from accordions while the disabled state is active.
// This to prevent users from going to click on something and then having it immediately
// disappear, or worse, change to a different link that they click by mistake.
.mw-block-fieldset[ disabled ] .cdx-accordion {
opacity: @opacity-low;
pointer-events: none;
}
.mw-block-fieldset {
min-width: unset;
// Unset font-size until T377902 is resolved.
font-size: unset;
legend {
// Match font-size of accordion labels. T383921.
font-size: @font-size-medium;
font-weight: inherit;
}
legend .cdx-label__label,
legend .cdx-label__description {
margin-bottom: @spacing-50;
}
// We need :is-fieldset="true" on the outer <fieldset> for disabled state to propagate
// to children. :is-fieldset="true" forces a <legend> which we don't want.
& > legend:first-of-type {
display: none;
}
}
</style>
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sat, Oct 3, 14:16 (9 h, 27 m ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
55/88/3faba112afb7d9ad42b79e3a6836
Default Alt Text
SpecialBlock.vue (16 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment