From a6a65204c69399b8332c1894ee7ad7cece0fbeb5 Mon Sep 17 00:00:00 2001 From: Jon Robson Date: Thu, 19 Oct 2023 14:12:09 -0700 Subject: [PATCH] Revert "Generalize settings code" This reverts commit 6924a89b07d134ebb7bc333fc487c49f6701da94. Reason for revert: Breaks persistence of setting for anonymous users. Change-Id: I3efc20f44281c1c68c4162584388e33bb38c4848 --- .nycrc.json | 2 +- resources/dist/index.js | Bin 47963 -> 47703 bytes resources/dist/index.js.map.json | Bin 216580 -> 216430 bytes src/actionTypes.js | 1 - src/actions.js | 17 +---- src/changeListeners/index.js | 4 +- src/changeListeners/settings.js | 9 +-- src/changeListeners/syncUserSettings.js | 67 ++++++++++++++++ src/index.js | 21 ++--- src/integrations/mwpopups.js | 17 +---- src/reducers/preview.js | 7 +- src/reducers/settings.js | 12 --- src/ui/settingsDialog.js | 33 ++++---- src/ui/settingsDialogRenderer.js | 56 ++++---------- tests/node-qunit/actions.test.js | 16 ---- .../changeListeners/settings.test.js | 7 +- .../changeListeners/syncUserSettings.test.js | 72 ++++++++++++++++++ tests/node-qunit/reducers/settings.test.js | 57 +++----------- .../ui/settingsDialogRenderer.test.js | 3 +- webpack.config.js | 4 +- 20 files changed, 210 insertions(+), 195 deletions(-) create mode 100644 src/changeListeners/syncUserSettings.js create mode 100644 tests/node-qunit/changeListeners/syncUserSettings.test.js diff --git a/.nycrc.json b/.nycrc.json index c6efabc12..4d19b4740 100644 --- a/.nycrc.json +++ b/.nycrc.json @@ -14,7 +14,7 @@ "//": "Set the coverage percentage by category thresholds.", "statements": 80, - "branches": 68, + "branches": 70, "functions": 80, "lines": 90, diff --git a/resources/dist/index.js b/resources/dist/index.js index 48e5bda45b3d982f19469aa3812eabfa1f0a323f..b7e8d22ca40082a6f547389f3102daa90df87e0a 100644 GIT binary patch delta 5440 zcmahtZE#yvcJpkUFD1@~#EE0aA5t9cdvN`bY#f5@7sr-m%UE`7OLjhtQDo`Lk}bcd zy!T|;k+oq5ybQ}nlRZGlrV#JK0!bhN6qc~CQ2MnqJG)ELY3Tr^yR@BYr`wj@X}6u7 zwwa#$WJxdl>5S&--gD1A=iGD8=l$$I%l`U5%3lAwdM=&&`+7?mmxeDJ>+rkD`Y3|V zSZ6EYsACfxaMZwM#}=5y|4(rK9mg(M>)ZtII_kM`_|(zNoq%;t!VN&1^DeFlsIwXV z2-nX$t?+NoO8AAdksE<8oy}!bhx(RXFuX?CD3G`$i!!eWBm^H;ZiR+z)#U=wR>u`E z*J9m}GEg*ugyA!9KYY=$uRI}T;O5a?Wv(!|TI+U&iBoJ7i05!%w96d``aDB@0k5mA zr7cU6ptb5;1pcY@-JzryI5O-B_IZXpqy6r7EgjdxF`f#Ua7d$#x30R(u%3Q@*I;*l z-&j|m|KQN-68!e=-CPo?+MJzHo)TTis8mm>a$|(eh|R`+6O;HAvhyLeaZZ|;Y>%eo zupU?B#x�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 PmcYu<+T4BbY$^Lce@kCo delta 5531 zcmaJlZEzIFl{JzffRF-4;!Egj5z7p2jaL!^ZltkTR;v%|dv}olf!Cv**6yIy%xY#< zDW`AqVwrV;sN)+h@Rzv01wu+vhmWUDaJ(e6FM_m;2$%cct#f+3~rY@?Da< z^1a?~94Zv*?SB3G_3QWj=HGr-^1){%FaN5Ri==*CTUNqF;N$ul9LpOXMbP<%>56UL zG${x~7ggRQ4wHs8%ciEE+P0>L;ayi1cMwWlPOb-bxpu)dS2-xIZ6LVT!#7-8xkGT> z)dWd|{@PU!KXFyS`ep~$54)S2xJH=7@d%u4Zi2sQu7Dpj*O!b^_^5ds)KzYV=JJiO z+~QoL$hxTWnm{bL+45B`4$ZA~oCTw;H8>irTe%Q?t<~nD@OkT7IDcz*O;tz?93Jro zd%QzlU!O+`DHg2{!AH9p?y9!!IFh#J&KOULEi9VUj+=UYl&y%v!TwJi!%;}iDQ3MY zPaJEXit8a;GxYi?o(hW$Y|AQKY_qy%aGONr#bf~|!EmxfBNO!4(vmBgClF5Gq6>>N zVk~Y&cv~PU`1cSL`N=(1++6C3J)29oNw~T15$D2fWal&7E+9qOE)ev>#9y4@0&sSJ zHQXgta&xdnsw?*w@eoaUrBfWlJv-r(c1P(Xuff}%EpTr~D;I=N$7OCLRU*H_tq&c)QkIHE#bVNcq%zR z{Gi-ii*aIMlG|YrOeR*Ws*h(Vx|G1sA1(GMj8^K(O7`aqqa2J2fyjP zS?gn})0G)2`FI_bNUe~m#h8&uW~VQ8tFLk8N_OL{44)(`<(vT&7#W^CPD8dhO&2T# ziZe=#=kr*3vs~0v$p&#`N0~zm8G1;u@m~1%k*2+cHE}c*UGG3=)hthhhNh`1(Fyxl z68*P5tJFd082wq9onPyq`sB8Z46;6L@ov*p7Q`vjnBjGCHcrh2KaC=@hUtz*d55OQ z;L3nJ<3>9X^tQY3U>KhoPPo>@|rF_qajM*hhN{k zCgYQIX^DsX5)SC}H7-+mme+}i&qME;GN~1jisgj{kX5oNQ!%EPRJVr=m9|sSq%+1G zMo$cz$;QC$rfyeTA;B)2LMMK9H}NS=r$L{+A8y8uA|6_Uh?7>U9mVbO-) z`?tfpk8Xn2K-EUnWJ8|KbiA=U`NQx)fWW5#iId=HKrB7XQ}{Tt5!N2uzLnu1QEGd2 zWipB?%%27b?u6LEl^lh{!TlTU3H=xoc@qESaOw|3!NKEmq$P3Fvl1V4B`0Q2TnPW8|EMZB^s5qr6m>TLyBpl z4kN^vnnJ-#kEL+9{lzctg%5)_YesXSl3i!Yh#}ewF-m&*okw-JqHKY^ zv^@3m2b)STV3mhPaSVn=H?v@4Yj%{P0j84+rE)_vvEiRW_f+a=pt-a`X?oj)FR2O6 zfMpfd6z)iEqiae^P0GWlUI`~pu7>V#CFe^`gxg9vGj;Cxo28r%1A2_J;cflzasE}q zf%9jLuXA>)Irby2M5FL-#V!!dO)iCI(km4T8O)-Y_o1_;qoz^x!c^S+w4}tx!|dp0 zc=O@A;j#D!oC2}A8x?W(0wxhO;dT}HM1(s4=MuG;ZeLE+mPhGCMogFyXW)ZGf(yf` zg&nE+`L!GuPMuoV%5iGy%85#DrHYiCP8K**?=RMI+zI&Yl4}Lpgw&tfp8RzX3wRiDOzwD6+V4P zubx7bT#i5+7wt%VW>QzQDCXy-hqtcE%0z|B4~MxKSb3_08-=b@lQ>>Gm8?vpk7t7H zB8^H0t|4~HiEn)HkvCQ_sm!IGJN>{46vka=Qp>v2S)2e&5$%o@R2EAJV@fnmEkTS@ zJ#0rBWVsmC_%o*?ye-6%$P{+*$o{F z855aAvz@21D@fNP((r_Ueo`htFMAka6|;~;(*KfB#K zlEd{3^m+OnB#7&7Z@|+V^atENB<9WoyhB|MGLT<-h6hImyaBI+9EW$#HZJ$!&WO9m zJJ#nN4-O3X^mq~7!+^dFEX$3r2;}b_9(RzTVu;r}W{?JpLA`xlc<*d6 zXtk%;J=BBHE|@-7%^il*=Lie5umS$@+zxI&_0hRjjz5^SwyZU<$RGu1rdXhaX2ZIj zgzM*OVKAh--y7Q=LdQZ71?!t{_sfpCQSPUFFH^NUzxXfXi-`^0dHQ|3df#Yy*W?Ahf%_q zrn;1FFUH|3PmY&o>vpDL5`ten>Bjo4@j|<+|BJ-?iitOVakm#{FVs{F7USHH3^=%1 z`00f`=&=_s)L;^AdFne=)aDnHgia{O1PP0H11BCA!fITZljst%WRR=sQ8oJU`1Mnx!D3ya9*mb z^XAux9L1o61rjzrSQ=~kbSDM%(qE&@{_E0F90xBa$~<{|2CXgg-?pxT@~1bI1&d(! zKHbUruPE_9WSAK$rzw)iYUG(~2TpcSrUbJ>U(_U6k7lqT$+*9T&+W*DX zdid!xQdw6K%<*g+RuYGvZQ?X|~Lzu|^^|^a6MsR=o-kx?U&m>rrF{Oox zEn@|3(LS^zDu(e{jzoCGh{a=8cT@>m4uLddJVE)u#T$G%X=vb^oi>){8ORoR_{QC3 z4BC=`LhO0xiu69t>fn#lYwOtVKDL|oDV=Dn6vXRiV%U6u~7>aRb&q8UJ?JW}G@OZR+r|7o;z-Ww0|IF~gHVd=75!HJdaVA0CZpi#&PWr4(Tp5eM%$MyoT;*$YP*A})FR8(mc zabh@QK8toob5WGFI}O-8G${CI33deAhkBhR>b=-VnEXy3`$`61zQycs|1EEoMika{ zm^>OeHpBQ6N4Ni_x&eMavQEr*@+c*d&g1tri;tnRlA>5nFtv8;(i9~*`A8F)Bp-rXsivLS@}W{<>F|)}f?v#C6v+q zu>!pWn6Fw@lwy0w8X*_ctZgcxY2JoEyjs0+CR+dtojhwTh+oF9A3Q9t*jUYaePR@u za?S|q7vSh?6b<0}ul>uiTV!V1m7w_5!X}i2h(Y3Lb?k!(nNCy7HO|<%xJ6}B(N#l7 z!Q%eN?fcjq6jdjv5ypbknYqFf)sSoa7=Y(gBNU&(Zx5mZ6|Zl@&tN03*Q~;f5Rb8P qc;xjMeo}k)b^j)VB+yrlgvi``J`joP)3TN%aOp^Os{1?ZOa32}aFv(< diff --git a/resources/dist/index.js.map.json b/resources/dist/index.js.map.json index 7d1db284947f5c129c6d1123e91a894dccfc31c3..33d61f816ed3ebf72ef8ab9dfeac326c4c1230dc 100644 GIT binary patch delta 23638 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 delta 24182 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 L`RDNX&iMZWDPtCy diff --git a/src/actionTypes.js b/src/actionTypes.js index 61063db11..ebe79a6f3 100644 --- a/src/actionTypes.js +++ b/src/actionTypes.js @@ -5,7 +5,6 @@ export default { BOOT: 'BOOT', LINK_DWELL: 'LINK_DWELL', - REGISTER_SETTING: 'REGISTER_SETTING', ABANDON_START: 'ABANDON_START', ABANDON_END: 'ABANDON_END', LINK_CLICK: 'LINK_CLICK', diff --git a/src/actions.js b/src/actions.js index 3ceb7c651..1b08288a1 100644 --- a/src/actions.js +++ b/src/actions.js @@ -72,22 +72,6 @@ export function boot( }; } -/** - * Registers a page preview setting for anonymous users. - * - * @param {string} name setting name which is used for storage and deriving associated - * messages. - * @param {boolean} enabled is the feature enabled by default? - * @return {Object} - */ -export function registerSetting( name, enabled ) { - return { - type: types.REGISTER_SETTING, - name, - enabled - }; -} - /** * Represents Page Previews fetching data via the gateway. * @@ -229,6 +213,7 @@ export function linkDwell( title, el, measures, gateway, generateToken, type ) { return promise.then( () => { const previewState = getState().preview; const enabledValue = previewState.enabled[ type ]; + // Note: Only reference previews and default previews can be disabled at this point. // If there is no UI the enabledValue is always true. const isEnabled = typeof enabledValue === 'undefined' ? true : enabledValue; diff --git a/src/changeListeners/index.js b/src/changeListeners/index.js index 9f070d0bb..9fd0593cc 100644 --- a/src/changeListeners/index.js +++ b/src/changeListeners/index.js @@ -4,6 +4,7 @@ import pageviews from './pageviews'; import render from './render'; import settings from './settings'; import statsv from './statsv'; +import syncUserSettings from './syncUserSettings'; export default { footerLink, @@ -11,5 +12,6 @@ export default { pageviews, render, settings, - statsv + statsv, + syncUserSettings }; diff --git a/src/changeListeners/settings.js b/src/changeListeners/settings.js index e6cde3093..6a4d37f01 100644 --- a/src/changeListeners/settings.js +++ b/src/changeListeners/settings.js @@ -13,19 +13,12 @@ export default function settings( boundActions, render ) { // Nothing to do on initialization return; } - if ( - settingsObj && - Object.keys( oldState.settings.keyValues ).length !== Object.keys( newState.settings.keyValues ).length - ) { - // the number of settings changed so force it to be repainted. - settingsObj.refresh( newState.settings.keyValues ); - } // Update global modal visibility if ( oldState.settings.shouldShow === false && newState.settings.shouldShow ) { // Lazily instantiate the settings UI if ( !settingsObj ) { - settingsObj = render( boundActions, newState.settings.keyValues ); + settingsObj = render( boundActions ); settingsObj.appendTo( document.body ); } diff --git a/src/changeListeners/syncUserSettings.js b/src/changeListeners/syncUserSettings.js new file mode 100644 index 000000000..fe6cf67a1 --- /dev/null +++ b/src/changeListeners/syncUserSettings.js @@ -0,0 +1,67 @@ +/** + * @module changeListeners/syncUserSettings + */ + +import { previewTypes } from '../preview/model'; + +/** + * Creates an instance of the user settings sync change listener. + * + * This change listener syncs certain parts of the state tree to user + * settings when they change. + * + * Used for: + * + * * Enabled state: If the previews are enabled or disabled. + * * Preview count: When the user dwells on a link for long enough that + * a preview is shown, then their preview count will be incremented (see + * `reducers/eventLogging.js`, and is persisted to local storage. + * + * @param {ext.popups.UserSettings} userSettings + * @return {ext.popups.ChangeListener} + */ +export default function syncUserSettings( userSettings ) { + return ( oldState, newState ) => { + syncIfChanged( + oldState, newState, 'preview.enabled.' + previewTypes.TYPE_PAGE, + userSettings.storePagePreviewsEnabled + ); + syncIfChanged( + oldState, newState, 'preview.enabled.' + previewTypes.TYPE_REFERENCE, + userSettings.storeReferencePreviewsEnabled + ); + }; +} + +/** + * Given a state tree, reducer and property, safely return the value of the + * property if the reducer and property exist + * + * @param {Object} state tree + * @param {string} path dot-separated path in the state tree + * @return {*} + */ +function get( state, path ) { + return path.split( '.' ).reduce( + ( element, key ) => element && element[ key ], + state + ); +} + +/** + * Calls a sync function if the property prop on the property reducer on + * the state trees has changed value. + * + * @param {Object} oldState + * @param {Object} newState + * @param {string} path dot-separated path in the state tree + * @param {Function} sync function to be called with the newest value if + * changed + * @return {void} + */ +function syncIfChanged( oldState, newState, path, sync ) { + const current = get( newState, path ); + if ( oldState && ( get( oldState, path ) !== current ) ) { + sync( current ); + } +} diff --git a/src/index.js b/src/index.js index d4a51fca5..54bacf325 100644 --- a/src/index.js +++ b/src/index.js @@ -118,6 +118,8 @@ function registerChangeListeners( registerChangeListener( store, changeListeners.render( previewBehavior ) ); registerChangeListener( store, changeListeners.statsv( registerActions, statsvTracker ) ); + registerChangeListener( + store, changeListeners.syncUserSettings( userSettings ) ); registerChangeListener( store, changeListeners.settings( registerActions, settingsDialog ) ); registerChangeListener( store, @@ -190,11 +192,15 @@ function handleDOMEventIfEligible( handler ) { referenceGateway = createReferenceGateway(), userSettings = createUserSettings( mw.storage ), referencePreviewsState = isReferencePreviewsEnabled( mw.user, userSettings, mw.config ), - settingsDialog = createSettingsDialogRenderer(), + settingsDialog = createSettingsDialogRenderer( referencePreviewsState !== null ), experiments = createExperiments( mw.experiments ), statsvTracker = getStatsvTracker( mw.user, mw.config, experiments ), pageviewTracker = getPageviewTracker( mw.config ), - pagePreviewState = createIsPagePreviewsEnabled( mw.user, userSettings, mw.config ); + initiallyEnabled = { + [ previewTypes.TYPE_PAGE ]: + createIsPagePreviewsEnabled( mw.user, userSettings, mw.config ), + [ previewTypes.TYPE_REFERENCE ]: referencePreviewsState + }; // If debug mode is enabled, then enable Redux DevTools. if ( mw.config.get( 'debug' ) || @@ -219,7 +225,7 @@ function handleDOMEventIfEligible( handler ) { ); boundActions.boot( - {}, + initiallyEnabled, mw.user, userSettings, mw.config, @@ -231,15 +237,13 @@ function handleDOMEventIfEligible( handler ) { * extensions can query it (T171287) */ mw.popups = createMediaWikiPopupsObject( - store, registerModel, registerPreviewUI, registerGatewayForPreviewType, - boundActions.registerSetting + store, registerModel, registerPreviewUI, registerGatewayForPreviewType ); - if ( pagePreviewState !== null ) { + if ( initiallyEnabled[ previewTypes.TYPE_PAGE ] !== null ) { const excludedLinksSelector = EXCLUDED_LINK_SELECTORS.join( ', ' ); // Register default preview type mw.popups.register( { - enabled: pagePreviewState, type: previewTypes.TYPE_PAGE, selector: `#mw-content-text a[href][title]:not(${excludedLinksSelector})`, delay: FETCH_COMPLETE_TARGET_DELAY - FETCH_START_DELAY, @@ -253,10 +257,9 @@ function handleDOMEventIfEligible( handler ) { ] } ); } - if ( referencePreviewsState !== null ) { + if ( initiallyEnabled[ previewTypes.TYPE_REFERENCE ] !== null ) { // Register the reference preview type mw.popups.register( { - enabled: referencePreviewsState, type: previewTypes.TYPE_REFERENCE, selector: '#mw-content-text .reference a[ href*="#" ]', delay: FETCH_DELAY_REFERENCE_TYPE, diff --git a/src/integrations/mwpopups.js b/src/integrations/mwpopups.js index 85d162a34..83f49edac 100644 --- a/src/integrations/mwpopups.js +++ b/src/integrations/mwpopups.js @@ -12,12 +12,9 @@ import { previewTypes } from '../preview/model'; * @param {Function} registerModel allows extensions to register custom preview handlers. * @param {Function} registerPreviewUI allows extensions to register custom preview renderers. * @param {Function} registerGatewayForPreviewType allows extensions to register gateways for preview types. - * @param {Function} registerSetting * @return {Object} external Popups interface */ -export default function createMwPopups( store, registerModel, registerPreviewUI, - registerGatewayForPreviewType, registerSetting -) { +export default function createMwPopups( store, registerModel, registerPreviewUI, registerGatewayForPreviewType ) { return { /** * @return {boolean} If Page Previews are currently active @@ -62,7 +59,7 @@ export default function createMwPopups( store, registerModel, registerPreviewUI, * @param {PopupModule} module */ register: function ( module ) { - const { type, selector, gateway, renderFn, subTypes, delay, init, enabled } = module; + const { type, selector, gateway, renderFn, subTypes, delay, init } = module; if ( !type || !selector || !gateway ) { throw new Error( `Registration of Popups custom preview type "${type}" failed: You must specify a type, a selector, and a gateway.` @@ -71,16 +68,6 @@ export default function createMwPopups( store, registerModel, registerPreviewUI, registerModel( type, selector, delay ); registerGatewayForPreviewType( type, gateway ); registerPreviewUI( type, renderFn ); - // Only show if doesn't exist. - if ( mw.message( `popups-settings-option-${type}` ).exists() ) { - registerSetting( type, enabled === undefined ? true : enabled ); - /* global process */ - } else if ( process.env.NODE_ENV !== 'production' ) { - mw.log.warn( - `[Popups] No setting for ${type} registered. -Please create message with key "popups-settings-option-${type}" if this is a mistake.` - ); - } if ( subTypes ) { subTypes.forEach( function ( subTypePreview ) { registerPreviewUI( subTypePreview.type, subTypePreview.renderFn ); diff --git a/src/reducers/preview.js b/src/reducers/preview.js index 321c51337..78fc63985 100644 --- a/src/reducers/preview.js +++ b/src/reducers/preview.js @@ -28,12 +28,7 @@ export default function preview( state, action ) { return nextState( state, { enabled: action.initiallyEnabled } ); - case actionTypes.REGISTER_SETTING: - return nextState( state, { - enabled: Object.assign( {}, state.enabled, { - [ action.name ]: action.enabled - } ) - } ); + case actionTypes.SETTINGS_CHANGE: { return nextState( state, { enabled: action.newValue diff --git a/src/reducers/settings.js b/src/reducers/settings.js index 3489692b3..a921201c1 100644 --- a/src/reducers/settings.js +++ b/src/reducers/settings.js @@ -12,7 +12,6 @@ export default function settings( state, action ) { if ( state === undefined ) { state = { shouldShow: false, - keyValues: {}, showHelp: false, shouldShowFooterLink: false }; @@ -58,23 +57,12 @@ export default function settings( state, action ) { shouldShowFooterLink: anyDisabled } ); } - case actionTypes.REGISTER_SETTING: { - const keyValues = Object.assign( {}, state.keyValues, { - [ action.name ]: action.enabled - } ); - return nextState( state, { - keyValues, - shouldShowFooterLink: state.shouldShowFooterLink || !action.enabled - } ); - } case actionTypes.BOOT: { // Warning, when the state is null the user can't re-enable this popup type! const anyDisabled = Object.keys( action.initiallyEnabled ) .some( ( type ) => action.initiallyEnabled[ type ] === false ); - const keyValues = Object.assign( {}, action.initiallyEnabled ); return nextState( state, { - keyValues, shouldShowFooterLink: action.user.isAnon && anyDisabled } ); } diff --git a/src/ui/settingsDialog.js b/src/ui/settingsDialog.js index f32f85117..09ef368ad 100644 --- a/src/ui/settingsDialog.js +++ b/src/ui/settingsDialog.js @@ -3,28 +3,33 @@ */ import { renderSettingsDialog } from './templates/settingsDialog/settingsDialog'; +import { previewTypes } from '../preview/model'; /** * Create the settings dialog shown to anonymous users. * - * @param {Object} keyValues + * @param {boolean} referencePreviewsAvaliable * @return {HTMLElement} settings dialog */ -export function createSettingsDialog( keyValues ) { - const choices = Object.keys( keyValues ).map( ( id ) => ( +export function createSettingsDialog( referencePreviewsAvaliable ) { + const choices = [ { - id, - // This can produce: - // * popups-settings-option-preview - // * popups-settings-option-reference - name: mw.msg( `popups-settings-option-${id}` ), - // This can produce: - // * popups-settings-option-preview-description - // * popups-settings-option-reference-description - description: mw.msg( `popups-settings-option-${id}-description` ), - isChecked: keyValues[ id ] + id: previewTypes.TYPE_PAGE, + name: mw.msg( 'popups-settings-option-page' ), + description: mw.msg( 'popups-settings-option-page-description' ) + }, + { + id: previewTypes.TYPE_REFERENCE, + name: mw.msg( 'popups-settings-option-reference' ), + description: mw.msg( 'popups-settings-option-reference-description' ) } - ) ); + ]; + + // TODO: Remove when not in Beta any more + if ( !referencePreviewsAvaliable ) { + // Anonymous users can't access reference previews as long as they are in beta + choices.splice( 1, 1 ); + } return renderSettingsDialog( { heading: mw.msg( 'popups-settings-title' ), diff --git a/src/ui/settingsDialogRenderer.js b/src/ui/settingsDialogRenderer.js index af70b5c6f..04c20ed6c 100644 --- a/src/ui/settingsDialogRenderer.js +++ b/src/ui/settingsDialogRenderer.js @@ -4,34 +4,14 @@ import { createSettingsDialog } from './settingsDialog'; -const initDialog = ( boundActions, keyValues ) => { - const dialog = createSettingsDialog( keyValues ); - - // Setup event bindings - dialog.querySelector( '.save' ).addEventListener( 'click', () => { - boundActions.saveSettings( - Array.from( dialog.querySelectorAll( 'input' ) ).reduce( - ( enabled, el ) => { - enabled[ el.value ] = el.matches( ':checked' ); - return enabled; - }, - {} - ) - ); - } ); - - dialog.querySelector( '.okay' ).addEventListener( 'click', boundActions.hideSettings ); - dialog.querySelector( '.close' ).addEventListener( 'click', boundActions.hideSettings ); - return dialog; -}; - /** * Creates a render function that will create the settings dialog and return * a set of methods to operate on it * + * @param {boolean} referencePreviewsAvaliable * @return {Function} render function */ -export default function createSettingsDialogRenderer() { +export default function createSettingsDialogRenderer( referencePreviewsAvaliable ) { /** * Cached settings dialog * @@ -49,32 +29,28 @@ export default function createSettingsDialogRenderer() { * Renders the relevant form and labels in the settings dialog * * @param {Object} boundActions - * @param {Object} keyValues * @return {Object} object with methods to affect the rendered UI */ - return ( boundActions, keyValues ) => { + return ( boundActions ) => { if ( !dialog ) { + dialog = createSettingsDialog( referencePreviewsAvaliable ); overlay = document.createElement( 'div' ); overlay.classList.add( 'mwe-popups-overlay' ); - dialog = initDialog( boundActions, keyValues ); + + // Setup event bindings + + dialog.querySelector( '.save' ).addEventListener( 'click', () => { + const enabled = {}; + Array.prototype.forEach.call( dialog.querySelectorAll( 'input' ), ( el ) => { + enabled[ el.value ] = el.matches( ':checked' ); + } ); + boundActions.saveSettings( enabled ); + } ); + dialog.querySelector( '.close' ).addEventListener( 'click', boundActions.hideSettings ); + dialog.querySelector( '.okay' ).addEventListener( 'click', boundActions.hideSettings ); } return { - /** - * Re-initialize the dialog when the available settings have changed. - * - * @param {Object} keyValuesNew updated key value pairs - */ - refresh( keyValuesNew ) { - if ( dialog ) { - const parent = dialog.parentNode; - dialog.remove(); - dialog = initDialog( boundActions, keyValuesNew ); - if ( parent ) { - dialog.appendTo( parent ); - } - } - }, /** * Append the dialog and overlay to a DOM element * diff --git a/tests/node-qunit/actions.test.js b/tests/node-qunit/actions.test.js index 6ed6bf1ef..c406941a9 100644 --- a/tests/node-qunit/actions.test.js +++ b/tests/node-qunit/actions.test.js @@ -60,22 +60,6 @@ QUnit.test( '#boot', ( assert ) => { ); } ); -QUnit.test( '#registerSetting', ( assert ) => { - const action = actions.registerSetting( - 'foo', - false - ); - assert.deepEqual( - action, - { - type: actionTypes.REGISTER_SETTING, - name: 'foo', - enabled: false - }, - 'Setting action' - ); -} ); - /** * Stubs `wait.js` and adds the deferred and its promise as properties * of the module. diff --git a/tests/node-qunit/changeListeners/settings.test.js b/tests/node-qunit/changeListeners/settings.test.js index a08765bdf..5df2c4e81 100644 --- a/tests/node-qunit/changeListeners/settings.test.js +++ b/tests/node-qunit/changeListeners/settings.test.js @@ -10,24 +10,21 @@ QUnit.module( 'ext.popups/changeListeners/settings', { toggleHelp: this.sandbox.spy(), setEnabled: this.sandbox.spy() }; - const keyValues = {}; this.render.withArgs( 'actions' ).returns( this.rendered ); - this.defaultState = { settings: { shouldShow: false, keyValues } }; + this.defaultState = { settings: { shouldShow: false } }; this.showState = { - settings: { shouldShow: true, keyValues }, + settings: { shouldShow: true }, preview: { enabled: { page: true, reference: true } } }; this.showHelpState = { settings: { - keyValues, shouldShow: true, showHelp: true } }; this.hideHelpState = { settings: { - keyValues, shouldShow: true, showHelp: false } diff --git a/tests/node-qunit/changeListeners/syncUserSettings.test.js b/tests/node-qunit/changeListeners/syncUserSettings.test.js new file mode 100644 index 000000000..19469be11 --- /dev/null +++ b/tests/node-qunit/changeListeners/syncUserSettings.test.js @@ -0,0 +1,72 @@ +import syncUserSettings from '../../../src/changeListeners/syncUserSettings'; + +QUnit.module( 'ext.popups/changeListeners/syncUserSettings', { + beforeEach() { + this.userSettings = { + storePagePreviewsEnabled: this.sandbox.spy(), + storeReferencePreviewsEnabled: this.sandbox.spy() + }; + + this.changeListener = syncUserSettings( this.userSettings ); + } +} ); + +QUnit.test( + 'it shouldn\'t update the storage if the enabled state hasn\'t changed', + function ( assert ) { + const oldState = { preview: { enabled: { page: true } } }, + newState = { preview: { enabled: { page: true } } }; + + this.changeListener( undefined, newState ); + this.changeListener( oldState, newState ); + + assert.false( + this.userSettings.storePagePreviewsEnabled.called, + 'The user setting is unchanged.' + ); + } +); + +QUnit.test( 'it should update the storage if the enabled flag has changed', function ( assert ) { + const oldState = { preview: { enabled: { page: true } } }, + newState = { preview: { enabled: { page: false } } }; + + this.changeListener( oldState, newState ); + + assert.true( + this.userSettings.storePagePreviewsEnabled.calledWith( false ), + 'The user setting is disabled.' + ); +} ); + +QUnit.test( + 'it shouldn\'t update the storage if the reference preview state hasn\'t changed', + function ( assert ) { + const oldState = { preview: { enabled: { reference: true } } }, + newState = { preview: { enabled: { reference: true } } }; + + this.changeListener( undefined, newState ); + this.changeListener( oldState, newState ); + + assert.false( + this.userSettings.storeReferencePreviewsEnabled.called, + 'Reference previews are unchanged.' + ); + } +); + +QUnit.test( 'it should update the storage if the reference preview state has changed', function ( assert ) { + const oldState = { preview: { enabled: { reference: true } } }, + newState = { preview: { enabled: { reference: false } } }; + + this.changeListener( oldState, newState ); + + assert.false( + this.userSettings.storePagePreviewsEnabled.called, + 'Page previews are unchanged.' + ); + assert.true( + this.userSettings.storeReferencePreviewsEnabled.calledWith( false ), + 'Reference previews opt-out is stored.' + ); +} ); diff --git a/tests/node-qunit/reducers/settings.test.js b/tests/node-qunit/reducers/settings.test.js index d3f112417..4e3165dd9 100644 --- a/tests/node-qunit/reducers/settings.test.js +++ b/tests/node-qunit/reducers/settings.test.js @@ -10,7 +10,6 @@ QUnit.test( '@@INIT', ( assert ) => { state, { shouldShow: false, - keyValues: {}, showHelp: false, shouldShowFooterLink: false }, @@ -25,15 +24,15 @@ QUnit.test( 'BOOT with a single disabled popup type', ( assert ) => { user: { isAnon: true } }; assert.deepEqual( - settings( {}, action ).shouldShowFooterLink, - true, + settings( {}, action ), + { shouldShowFooterLink: true }, 'The boot state shows a footer link.' ); action.user.isAnon = false; assert.deepEqual( - settings( {}, action ).shouldShowFooterLink, - false, + settings( {}, action ), + { shouldShowFooterLink: false }, 'If the user is logged in, then it doesn\'t signal that the footer link should be shown.' ); } ); @@ -45,62 +44,26 @@ QUnit.test( 'BOOT with multiple popup types', ( assert ) => { user: { isAnon: true } }; assert.deepEqual( - settings( {}, action ).shouldShowFooterLink, - false, + settings( {}, action ), + { shouldShowFooterLink: false }, 'Footer link ignores unavailable popup types.' ); action.initiallyEnabled.reference = true; assert.deepEqual( - settings( {}, action ).shouldShowFooterLink, - false, + settings( {}, action ), + { shouldShowFooterLink: false }, 'Footer link is pointless when there is nothing to enable.' ); action.initiallyEnabled.reference = false; assert.deepEqual( - settings( {}, action ).shouldShowFooterLink, - true, + settings( {}, action ), + { shouldShowFooterLink: true }, 'Footer link appears when at least one popup type is disabled.' ); } ); -QUnit.test( 'REGISTER_SETTING that is disabled by default reveals footer link', ( assert ) => { - const REGISTER_SETTING_FOO_DISABLED = { - type: actionTypes.REGISTER_SETTING, - name: 'foo', - enabled: false - }; - const newState = settings( { - keyValues: {}, - shouldShowFooterLink: false - }, REGISTER_SETTING_FOO_DISABLED ); - - assert.deepEqual( - newState.shouldShowFooterLink, - true, - 'if one setting is registered as disabled, then the footer link is revealed.' - ); -} ); - -QUnit.test( 'REGISTER_SETTING that is enabled by default should not show footer link', ( assert ) => { - const REGISTER_SETTING_FOO_ENABLED = { - type: actionTypes.REGISTER_SETTING, - name: 'foo', - enabled: true - }; - const newState = settings( { - keyValues: {}, - shouldShowFooterLink: false - }, REGISTER_SETTING_FOO_ENABLED ); - - assert.deepEqual( - newState.keyValues.foo, - true, - 'keyValues is updated' - ); -} ); - QUnit.test( 'SETTINGS_SHOW', ( assert ) => { assert.deepEqual( settings( {}, { type: actionTypes.SETTINGS_SHOW } ), diff --git a/tests/node-qunit/ui/settingsDialogRenderer.test.js b/tests/node-qunit/ui/settingsDialogRenderer.test.js index 399942ebd..1cc4b39ff 100644 --- a/tests/node-qunit/ui/settingsDialogRenderer.test.js +++ b/tests/node-qunit/ui/settingsDialogRenderer.test.js @@ -43,14 +43,13 @@ QUnit.test( '#render', ( assert ) => { hideSettings() {} }, expected = { - refresh() {}, appendTo() {}, show() {}, hide() {}, toggleHelp() {}, setEnabled() {} }, - result = createSettingsDialogRenderer( mw.config )( boundActions, {} ); + result = createSettingsDialogRenderer( mw.config )( boundActions ); // Specifically NOT a deep equal. Only structure. assert.propEqual( diff --git a/webpack.config.js b/webpack.config.js index c4ffc9de3..7915cd69b 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -118,8 +118,8 @@ module.exports = ( env, argv ) => ( { // Minified uncompressed size limits for chunks / assets and entrypoints. Keep these numbers // up-to-date and rounded to the nearest 10th of a kibibyte so that code sizing costs are // well understood. Related to bundlesize minified, gzipped compressed file size tests. - maxAssetSize: 47.1 * 1024, - maxEntrypointSize: 47.1 * 1024, + maxAssetSize: 46.9 * 1024, + maxEntrypointSize: 46.9 * 1024, // The default filter excludes map files but we rename ours. assetFilter: ( filename ) => !filename.endsWith( srcMapExt )