3 class="mw-block-expiry-field"
7 {{ $i18n( 'block-expiry' ).text() }}
13 input-value="preset-duration"
15 {{ $i18n( 'block-expiry-preset' ).text() }}
16 <template v-if="expiryType === 'preset-duration'" #custom-input>
18 class="mw-block-expiry-field__preset-duration"
19 :status="presetDurationStatus"
20 :messages="presetDurationMessages"
23 v-if="expiryType === 'preset-duration'"
24 v-model:selected="presetDuration"
25 :menu-items="presetDurationOptions"
26 :default-label="$i18n( 'block-expiry-preset-placeholder' ).text()"
27 @update:selected="() => {
28 presetDurationStatus = 'default';
29 presetDurationMessages = {};
39 input-value="custom-duration"
41 {{ $i18n( 'block-expiry-custom' ).text() }}
42 <template v-if="expiryType === 'custom-duration'" #custom-input>
44 class="mw-block-expiry-field__custom-duration"
45 :status="customDurationStatus"
46 :messages="customDurationMessages"
48 <validating-text-input
49 v-model="customDurationNumber"
53 @update:status="( status, message ) => {
54 customDurationMessages = message;
55 customDurationStatus = status;
57 ></validating-text-input>
59 v-model:selected="customDurationUnit"
60 :menu-items="customDurationOptions"
69 input-value="datetime"
71 {{ $i18n( 'block-expiry-datetime' ).text() }}
72 <template v-if="expiryType === 'datetime'" #custom-input>
74 class="mw-block-expiry-field__datetime"
75 :status="datetimeStatus"
76 :messages="datetimeMessages"
78 <validating-text-input
79 v-if="expiryType === 'datetime'"
81 input-type="datetime-local"
83 :min="new Date().toISOString().slice( 0, 16 )"
85 @update:status="( status, message ) => {
86 datetimeMessages = message;
87 datetimeStatus = status;
89 ></validating-text-input>
97 const { defineComponent, ref, computed, watch } = require( 'vue' );
98 const { CdxField, CdxRadio, CdxSelect } = require( '@wikimedia/codex' );
99 const { storeToRefs } = require( 'pinia' );
100 const ValidatingTextInput = require( './ValidatingTextInput.js' );
101 const useBlockStore = require( '../stores/block.js' );
103 module.exports = exports = defineComponent( {
112 const store = useBlockStore();
113 const blockExpiryOptions = mw.config.get( 'blockExpiryOptions' );
114 const presetDurationOptions = Object.keys( blockExpiryOptions )
115 .map( ( key ) => ( { label: key, value: blockExpiryOptions[ key ] } ) )
116 // Don't include "other" in the preset options as it's handled separately in the new UI
117 .filter( ( option ) => option.value !== 'other' );
119 const customDurationOptions = [
120 { value: 'minutes', label: mw.msg( 'block-expiry-custom-minutes' ) },
121 { value: 'hours', label: mw.msg( 'block-expiry-custom-hours' ) },
122 { value: 'days', label: mw.msg( 'block-expiry-custom-days' ) },
123 { value: 'weeks', label: mw.msg( 'block-expiry-custom-weeks' ) },
124 { value: 'months', label: mw.msg( 'block-expiry-custom-months' ) },
125 { value: 'years', label: mw.msg( 'block-expiry-custom-years' ) }
128 const presetDuration = ref( null );
129 const presetDurationStatus = ref( 'default' );
130 const presetDurationMessages = ref( {} );
131 const customDurationNumber = ref( 1 );
132 const customDurationUnit = ref( 'hours' );
133 const customDurationStatus = ref( 'default' );
134 const customDurationMessages = ref( {} );
135 const datetime = ref( '' );
136 const datetimeStatus = ref( 'default' );
137 const datetimeMessages = ref( {} );
138 const expiryType = ref( 'preset-duration' );
140 const computedModelValue = computed( () => {
141 if ( expiryType.value === 'preset-duration' ) {
142 return presetDuration.value;
143 } else if ( expiryType.value === 'custom-duration' ) {
144 return `${ Number( customDurationNumber.value ) } ${ customDurationUnit.value }`;
146 return datetime.value;
151 * Set the form fields according to the given expiry.
153 * @param {string} given
155 function setDurationFromGiven( given ) {
156 const optionsContainsValue = ( opts, v ) => opts.some( ( option ) => option.value === v );
157 if ( mw.util.isInfinity( given ) ) {
158 expiryType.value = 'preset-duration';
159 // FIXME: Assumes that the "infinite" option exists.
160 // (It has to be for this form as there's no other way to specify infinite)
161 presetDuration.value = 'infinite';
162 } else if ( optionsContainsValue( presetDurationOptions, given ) ) {
163 expiryType.value = 'preset-duration';
164 presetDuration.value = given;
165 } else if ( /^\d+ [a-z]+$/.test( given ) ) {
166 const [ number, unit ] = given.split( ' ' );
167 // Normalize the unit to plural form, as used by customDurationOptions.
168 const unitPlural = unit.replace( /s?$/, 's' );
169 if ( optionsContainsValue( customDurationOptions, unitPlural ) ) {
170 expiryType.value = 'custom-duration';
171 customDurationNumber.value = Number( number );
172 customDurationUnit.value = unitPlural;
174 } else if ( /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/.test( given ) ) {
175 expiryType.value = 'datetime';
176 // Truncate longer datetime strings to be compatible with input type=datetime-local.
177 // This is also done in SpecialBlock.php
178 datetime.value = given.slice( 0, 16 );
180 // Unsupported format; Reset to defaults.
181 expiryType.value = 'preset-duration';
182 customDurationNumber.value = 1;
183 customDurationUnit.value = 'hours';
188 const { expiry } = storeToRefs( store );
190 // Update the store's expiry value when the computed value changes.
191 watch( computedModelValue, ( newValue ) => {
192 if ( newValue !== expiry.value ) {
193 // Remove browser-specific milliseconds from datetime for consistency.
194 if ( expiryType.value === 'datetime' ) {
195 newValue = newValue.replace( /\.000$/, '' );
197 expiry.value = newValue;
201 // Update the form fields when the store's expiry value changes.
202 watch( expiry, ( newValue ) => {
203 if ( newValue !== computedModelValue.value ) {
204 setDurationFromGiven( newValue );
206 }, { immediate: true } );
209 * The preset duration field is a dropdown that requires custom validation.
210 * We simply need to assert something is selected, but only do so after form submission.
212 watch( () => store.formSubmitted, ( submitted ) => {
213 if ( submitted && expiryType.value === 'preset-duration' && !presetDuration.value ) {
214 presetDurationStatus.value = 'error';
215 presetDurationMessages.value = { error: mw.msg( 'ipb_expiry_invalid' ) };
220 presetDurationOptions,
221 presetDurationStatus,
222 presetDurationMessages,
223 customDurationStatus,
224 customDurationMessages,
225 customDurationOptions,
227 customDurationNumber,
239 /* stylelint-disable plugin/no-unsupported-browser-features */
240 @import 'mediawiki.skin.variables.less';
242 .mw-block-expiry-field {
243 .cdx-radio__custom-input .cdx-label {
251 &__custom-duration .cdx-field__control {