* where bool indicates the checked status of the checkbox
* @return array
*/
- protected function getCheckboxesDefinition( $checked ) {
+ public function getCheckboxesDefinition( $checked ) {
global $wgUser;
$checkboxes = [];
private function getGoodIDs( $titles ) {
$result = [];
if ( is_array( $titles ) ) {
+ ( new LinkBatch( $titles ) )->execute();
+
foreach ( $titles as $title ) {
$pageID = $title->getArticleID();
if ( $pageID > 0 ) {
protected $caller;
/**
- * @param LinkTarget[] $arr Initial items to be added to the batch
+ * @param Traversable|LinkTarget[] $arr Initial items to be added to the batch
*/
public function __construct( $arr = [] ) {
foreach ( $arr as $item ) {
/**
* Create an update object from an array of Title objects, or a TitleArray object
*
- * @param Traversable|array $titles
+ * @param Traversable|Title[] $titles
* @param string[] $urlArr
* @return CdnCacheUpdate
*/
public static function newFromTitles( $titles, $urlArr = [] ) {
+ ( new LinkBatch( $titles ) )->execute();
/** @var Title $title */
foreach ( $titles as $title ) {
$urlArr = array_merge( $urlArr, $title->getCdnUrls() );
public function selectSQLText(
$table, $vars, $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
) {
+ if ( is_string( $options ) ) {
+ $options = [ $options ];
+ }
+
// Change the FOR UPDATE option as necessary based on the join conditions. Then pass
// to the parent function to get the actual SQL text.
// In Postgres when using FOR UPDATE, only the main table and tables that are inner joined
$forUpdateKey = array_search( 'FOR UPDATE', $options, true );
if ( $forUpdateKey !== false && $join_conds ) {
unset( $options[$forUpdateKey] );
+ $options['FOR UPDATE'] = [];
+
+ // All tables not in $join_conds are good
+ foreach ( $table as $alias => $name ) {
+ if ( is_numeric( $alias ) ) {
+ $alias = $name;
+ }
+ if ( !isset( $join_conds[$alias] ) ) {
+ $options['FOR UPDATE'][] = $alias;
+ }
+ }
foreach ( $join_conds as $table_cond => $join_cond ) {
if ( 0 === preg_match( '/^(?:LEFT|RIGHT|FULL)(?: OUTER)? JOIN$/i', $join_cond[0] ) ) {
$options['FOR UPDATE'][] = $table_cond;
}
}
+
+ // Quote alias names so $this->tableName() won't mangle them
+ $options['FOR UPDATE'] = array_map( function ( $name ) use ( $table ) {
+ return isset( $table[$name] ) ? $this->addIdentifierQuotes( $name ) : $name;
+ }, $options['FOR UPDATE'] );
}
if ( isset( $options['ORDER BY'] ) && $options['ORDER BY'] == 'NULL' ) {
* @param IDatabase $db Object throwing the error
* @param string $error Error text
*/
- function __construct( IDatabase $db = null, $error = 'unknown error' ) {
+ public function __construct( IDatabase $db = null, $error = 'unknown error' ) {
$msg = 'Cannot access the database';
if ( trim( $error ) != '' ) {
$msg .= ": $error";
* @param IDatabase $db Object which threw the error
* @param string $error A simple error message to be used for debugging
*/
- function __construct( IDatabase $db = null, $error ) {
+ public function __construct( IDatabase $db = null, $error ) {
$this->db = $db;
parent::__construct( $error );
}
/** @var string[] Message parameters */
protected $params;
- function __construct( IDatabase $db = null, $error, array $params = [] ) {
+ public function __construct( IDatabase $db = null, $error, array $params = [] ) {
parent::__construct( $db, $error );
$this->params = $params;
}
* @param string $sql
* @param string $fname
*/
- function __construct( IDatabase $db, $error, $errno, $sql, $fname ) {
+ public function __construct( IDatabase $db, $error, $errno, $sql, $fname ) {
if ( $db instanceof Database && $db->wasConnectionError( $errno ) ) {
$message = "A connection error occured. \n" .
"Query: $sql\n" .
* @ingroup Database
*/
class DBTransactionSizeError extends DBTransactionError {
- function getKey() {
+ public function getKey() {
return 'transaction-duration-limit-exceeded';
}
}
'wgStructuredChangeFiltersSavedQueriesPreferenceName',
'rcfilters-wl-saved-queries'
);
+ $output->addJsConfigVars(
+ 'wgStructuredChangeFiltersEditWatchlistUrl',
+ SpecialPage::getTitleFor( 'EditWatchlist' )->getLocalURL()
+ );
}
}
$showUpdatedMarker = $this->getConfig()->get( 'ShowUpdatedMarker' );
// Show watchlist header
- $form .= "<p>";
+ $watchlistHeader = '';
if ( $numItems == 0 ) {
- $form .= $this->msg( 'nowatchlist' )->parse() . "\n";
+ $watchlistHeader = $this->msg( 'nowatchlist' )->parse();
} else {
- $form .= $this->msg( 'watchlist-details' )->numParams( $numItems )->parse() . "\n";
+ $watchlistHeader .= $this->msg( 'watchlist-details' )->numParams( $numItems )->parse() . "\n";
if ( $this->getConfig()->get( 'EnotifWatchlist' )
&& $user->getOption( 'enotifwatchlistpages' )
) {
- $form .= $this->msg( 'wlheader-enotif' )->parse() . "\n";
+ $watchlistHeader .= $this->msg( 'wlheader-enotif' )->parse() . "\n";
}
if ( $showUpdatedMarker ) {
- $form .= $this->msg( 'wlheader-showupdated' )->parse() . "\n";
+ $watchlistHeader .= $this->msg(
+ $this->isStructuredFilterUiEnabled() ?
+ 'rcfilters-watchlist-showupdated' :
+ 'wlheader-showupdated'
+ )->parse() . "\n";
}
}
- $form .= "</p>";
+ $form .= Html::rawElement(
+ 'div',
+ [ 'class' => 'watchlistDetails' ],
+ $watchlistHeader
+ );
if ( $numItems > 0 && $showUpdatedMarker ) {
$form .= Xml::openElement( 'form', [ 'method' => 'post',
wfDebugLog( 'fileupload', $status );
if ( $status->isOK() ) {
- wfDebugLog( 'fileupload', 'Download by URL completed successfuly.' );
+ wfDebugLog( 'fileupload', 'Download by URL completed successfully.' );
} else {
wfDebugLog(
'fileupload',
"rcfilters-empty-filter": "No active filters. All contributions are shown.",
"rcfilters-filterlist-title": "Filters",
"rcfilters-filterlist-whatsthis": "How do these work?",
- "rcfilters-filterlist-feedbacklink": "Provide feedback on the new (beta) filters",
+ "rcfilters-filterlist-feedbacklink": "Tell us what you think about these (new) filtering tools",
"rcfilters-highlightbutton-title": "Highlight results",
"rcfilters-highlightmenu-title": "Select a color",
"rcfilters-highlightmenu-help": "Select a color to highlight this property",
"rcfilters-liveupdates-button-title-on": "Turn off live updates",
"rcfilters-liveupdates-button-title-off": "Display new changes as they happen",
"rcfilters-watchlist-markSeen-button": "Mark all changes as seen",
+ "rcfilters-watchlist-editWatchlist-button": "Edit your list of watched pages",
+ "rcfilters-watchlist-showupdated": "Changes to pages you haven't visited since the changes occurred are in <strong>bold</strong>, with solid markers.",
"rcnotefrom": "Below {{PLURAL:$5|is the change|are the changes}} since <strong>$3, $4</strong> (up to <strong>$1</strong> shown).",
"rclistfromreset": "Reset date selection",
"rclistfrom": "Show new changes starting from $2, $3",
"rcfilters-liveupdates-button-title-on": "Title for the button to enable or disable live updates on [[Special:RecentChanges]] when the feature is ON.",
"rcfilters-liveupdates-button-title-off": "Title for the button to enable or disable live updates on [[Special:RecentChanges]] when the feature is OFF.",
"rcfilters-watchlist-markSeen-button": "Label for the button to mark all changes as seen on [[Special:Watchlist]] when using the structured filters interface.",
+ "rcfilters-watchlist-editWatchlist-button": "Label for the button to edit the watched pages on [[Special:Watchlist]] when using the structured filters interface.",
+ "rcfilters-watchlist-showupdated": "Message at the top of [[Special:Watchlist]] when the Structured filters are enabled that describes what unseen changes look like.",
"rcnotefrom": "This message is displayed at [[Special:RecentChanges]] when viewing recentchanges from some specific time.\n\nThe corresponding message is {{msg-mw|Rclistfrom}}.\n\nParameters:\n* $1 - the maximum number of changes that are displayed\n* $2 - (Optional) a date and time\n* $3 - a date\n* $4 - a time\n* $5 - Number of changes are displayed, for use with PLURAL",
"rclistfromreset": "Used on [[Special:RecentChanges]] to reset a selection of a certain date range.",
"rclistfrom": "Used on [[Special:RecentChanges]]. Parameters:\n* $1 - (Currently not use) date and time. The date and the time adds to the rclistfrom description.\n* $2 - time. The time adds to the rclistfrom link description (with split of date and time).\n* $3 - date. The date adds to the rclistfrom link description (with split of date and time).\n\nThe corresponding message is {{msg-mw|Rcnotefrom}}.",
comment_text BLOB NOT NULL,
comment_data BLOB
) /*$wgDBTableOptions*/;
-CREATE INDEX /*i*/comment_hash ON comment (comment_hash);
+CREATE INDEX /*i*/comment_hash ON /*_*/comment (comment_hash);
CREATE TABLE /*_*/revision_comment_temp (
revcomment_rev int unsigned NOT NULL,
CREATE TABLE image_comment_temp (
imgcomment_name TEXT NOT NULL,
- imgcomment_comment_id INTEGER NOT NULL,
- PRIMARY KEY (imgcomment_name, imgcomment_comment_id)
+ imgcomment_description_id INTEGER NOT NULL,
+ PRIMARY KEY (imgcomment_name, imgcomment_description_id)
);
-CREATE UNIQUE INDEX imgcomment_name ON image_comment_temp (imgcomment_rev);
+CREATE UNIQUE INDEX imgcomment_name ON image_comment_temp (imgcomment_name);
CREATE TABLE image_comment_temp (
imgcomment_name TEXT NOT NULL,
- imgcomment_comment_id INTEGER NOT NULL,
- PRIMARY KEY (imgcomment_name, imgcomment_comment_id)
+ imgcomment_description_id INTEGER NOT NULL,
+ PRIMARY KEY (imgcomment_name, imgcomment_description_id)
);
-CREATE UNIQUE INDEX imgcomment_name ON image_comment_temp (imgcomment_rev);
+CREATE UNIQUE INDEX imgcomment_name ON image_comment_temp (imgcomment_name);
CREATE TABLE oldimage (
oi_name TEXT NOT NULL,
comment_text BLOB NOT NULL,
comment_data BLOB
) /*$wgDBTableOptions*/;
-CREATE INDEX /*i*/comment_hash ON comment (comment_hash);
+CREATE INDEX /*i*/comment_hash ON /*_*/comment (comment_hash);
CREATE TABLE /*_*/revision_comment_temp (
revcomment_rev int unsigned NOT NULL,
comment_data BLOB
) /*$wgDBTableOptions*/;
-- Index used for deduplication.
-CREATE INDEX /*i*/comment_hash ON comment (comment_hash);
+CREATE INDEX /*i*/comment_hash ON /*_*/comment (comment_hash);
--
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.HighlightColorPickerWidget.js',
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.LiveUpdateButtonWidget.js',
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.MarkSeenButtonWidget.js',
+ 'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.RcTopSectionWidget.js',
+ 'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.WatchlistTopSectionWidget.js',
'resources/src/mediawiki.rcfilters/mw.rcfilters.HighlightColors.js',
'resources/src/mediawiki.rcfilters/mw.rcfilters.init.js',
],
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.SavedLinksListItemWidget.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.SaveFiltersPopupButtonWidget.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.LiveUpdateButtonWidget.less',
+ 'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.RcTopSectionWidget.less',
+ 'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.WatchlistTopSectionWidget.less',
],
'skinStyles' => [
'monobook' => [
'rcfilters-liveupdates-button-title-on',
'rcfilters-liveupdates-button-title-off',
'rcfilters-watchlist-markSeen-button',
+ 'rcfilters-watchlist-editWatchlist-button',
'rcfilters-other-review-tools',
'blanknamespace',
'namespaces',
return result;
};
+ /**
+ * Get an array of currently applied highlight colors
+ *
+ * @return {string[]} Currently applied highlight colors
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getCurrentlyUsedHighlightColors = function () {
+ var result = [];
+
+ this.getHighlightedItems().forEach( function ( filterItem ) {
+ var color = filterItem.getHighlightColor();
+
+ if ( result.indexOf( color ) === -1 ) {
+ result.push( color );
+ }
+ } );
+
+ return result;
+ };
+
/**
* Sanitize value group of a string_option groups type
* Remove duplicates and make sure to only use valid
this.filtersModel.toggleInvertedNamespaces( !!Number( parameters.invert ) );
// Update highlight state
- this.filtersModel.toggleHighlight( !!Number( parameters.highlight ) );
this.filtersModel.getItems().forEach( function ( filterItem ) {
var color = parameters[ filterItem.getName() + '_color' ];
if ( color ) {
filterItem.clearHighlightColor();
}
} );
+ this.filtersModel.toggleHighlight( !!Number( parameters.highlight ) );
// Check all filter interactions
this.filtersModel.reassessFilterInteractions();
* @private
*/
init: function () {
- var toplinksTitle,
- topLinksCookieName = 'rcfilters-toplinks-collapsed-state',
- topLinksCookie = mw.cookie.get( topLinksCookieName ),
- topLinksCookieValue = topLinksCookie || 'collapsed',
+ var $topLinks,
+ rcTopSection,
+ $watchlistDetails,
+ wlTopSection,
savedQueriesPreferenceName = mw.config.get( 'wgStructuredChangeFiltersSavedQueriesPreferenceName' ),
filtersModel = new mw.rcfilters.dm.FiltersViewModel(),
changesListModel = new mw.rcfilters.dm.ChangesListViewModel(),
.addClass( 'mw-rcfilters-ui-overlay' ),
filtersWidget = new mw.rcfilters.ui.FilterWrapperWidget(
controller, filtersModel, savedQueriesModel, changesListModel, { $overlay: $overlay } ),
- markSeenButton,
+ savedLinksListWidget = new mw.rcfilters.ui.SavedLinksListWidget(
+ controller, savedQueriesModel, { $overlay: $overlay }
+ ),
currentPage = mw.config.get( 'wgCanonicalNamespace' ) +
':' +
mw.config.get( 'wgCanonicalSpecialPageName' );
controller.replaceUrl();
if ( currentPage === 'Special:Recentchanges' ) {
- toplinksTitle = new OO.ui.ButtonWidget( {
- framed: false,
- indicator: topLinksCookieValue === 'collapsed' ? 'down' : 'up',
- flags: [ 'progressive' ],
- label: $( '<span>' ).append( mw.message( 'rcfilters-other-review-tools' ).parse() ).contents()
- } );
- $( '.mw-recentchanges-toplinks-title' ).replaceWith( toplinksTitle.$element );
- // Move the top links to a designated area so it's near the
- // 'saved filters' button and make it collapsible
- $( '.mw-recentchanges-toplinks' )
- .addClass( 'mw-rcfilters-ui-ready' )
- .makeCollapsible( {
- collapsed: topLinksCookieValue === 'collapsed',
- $customTogglers: toplinksTitle.$element
- } )
- .on( 'beforeExpand.mw-collapsible', function () {
- mw.cookie.set( topLinksCookieName, 'expanded' );
- toplinksTitle.setIndicator( 'up' );
- } )
- .on( 'beforeCollapse.mw-collapsible', function () {
- mw.cookie.set( topLinksCookieName, 'collapsed' );
- toplinksTitle.setIndicator( 'down' );
- } )
- .appendTo( '.mw-rcfilters-ui-filterWrapperWidget-top-placeholder' );
+ $topLinks = $( '.mw-recentchanges-toplinks' ).detach();
+
+ rcTopSection = new mw.rcfilters.ui.RcTopSectionWidget(
+ savedLinksListWidget, $topLinks
+ );
+ filtersWidget.setTopSection( rcTopSection.$element );
} // end Special:RC
if ( currentPage === 'Special:Watchlist' ) {
- markSeenButton = new mw.rcfilters.ui.MarkSeenButtonWidget( controller, changesListModel );
- $( 'form#mw-watchlist-resetbutton' ).detach();
- filtersWidget.prependToTopRow( markSeenButton );
+ $( '#contentSub, form#mw-watchlist-resetbutton' ).detach();
+ $watchlistDetails = $( '.watchlistDetails' ).detach().contents();
+
+ wlTopSection = new mw.rcfilters.ui.WatchlistTopSectionWidget(
+ controller, changesListModel, savedLinksListWidget, $watchlistDetails
+ );
+ filtersWidget.setTopSection( wlTopSection.$element );
} // end Special:WL
}
};
border: 0;
}
- .mw-recentchanges-toplinks {
- padding: 0 0.5em;
-
- .oo-ui-widget-enabled.oo-ui-buttonElement.oo-ui-buttonElement-frameless .oo-ui-buttonElement-button {
- padding: 0 2.5em 0 0.5em;
- }
-
- &-title,
- .mw-collapsible-text {
- // Same as the legend
- font-size: 0.85em;
- }
-
- &:not( .mw-collapsed ) {
- // Same as the legend
- border: 1px solid #ddd;
- }
-
- &:not( .mw-rcfilters-ui-ready ) {
- display: none;
- }
- }
-
.rcfilters-head {
min-height: 200px;
.animation-delay( 0s );
}
}
+
+ #contentSub,
+ p.watchlistDetails,
+ form#mw-watchlist-resetbutton {
+ display: none;
+ }
}
.mw-rcfilters-staticfilters-selected {
}
}
+// A mixin just for changesListWrapperWidget page, to output the scope of the widget
+// so it is before the rest of the rule; we need the li& to be in
+// between the wrapper scope and the color-cX class, which doesn't
+// work if the rules are inside the above widget LESS scope
+.highlight-changesListWrapperWidget( @bgcolor ) {
+ .mw-rcfilters-ui-changesListWrapperWidget li&,
+ .mw-rcfilters-ui-changesListWrapperWidget & tr:first-child,
+ .mw-rcfilters-ui-changesListWrapperWidget tr&.mw-rcfilters-ui-changesListWrapperWidget-enhanced-toplevel:not(.mw-rcfilters-ui-changesListWrapperWidget-enhanced-grey) td:not( :nth-child( -n+2 ) ),
+ .mw-rcfilters-ui-changesListWrapperWidget tr&.mw-rcfilters-ui-changesListWrapperWidget-enhanced-nested:not(.mw-rcfilters-ui-changesListWrapperWidget-enhanced-grey) td:not( :nth-child( -n+3 ) ) {
+ background-color: @bgcolor;
+ }
+}
+
// This mixin produces color mixes for two, three and four colors
.highlight-color-mix( @color1, @color2, @color3: false, @color4: false ) {
@highlight-color-class-var: ~'.mw-rcfilters-highlight-color-@{color1}.mw-rcfilters-highlight-color-@{color2}';
// Two colors
@{highlight-color-class-var} when ( @color3 = false ) and ( @color4 = false ) and not ( @color1 = false ), ( @color2 = false ) {
- background-color: tint( average( @@c1var, @@c2var ), 50% );
+ .highlight-changesListWrapperWidget( tint( average( @@c1var, @@c2var ), 50% ) );
}
// Three colors
@{highlight-color-class-var}.mw-rcfilters-highlight-color-@{color3} when ( @color4 = false ) and not ( @color3 = false ) {
@c3var: ~'highlight-@{color3}';
- background-color: tint( mix( @@c1var, average( @@c2var, @@c3var ), 33% ), 30% );
+ .highlight-changesListWrapperWidget( tint( mix( @@c1var, average( @@c2var, @@c3var ), 33% ), 30% ) );
}
// Four colors
@{highlight-color-class-var}.mw-rcfilters-highlight-color-@{color3}.mw-rcfilters-highlight-color-@{color4} when not ( @color4 = false ) {
@c3var: ~'highlight-@{color3}';
@c4var: ~'highlight-@{color4}';
- background-color: tint( mix( @@c1var, mix( @@c2var, average( @@c3var, @@c4var ), 25% ), 25% ), 25% );
+ .highlight-changesListWrapperWidget( tint( mix( @@c1var, mix( @@c2var, average( @@c3var, @@c4var ), 25% ), 25% ), 25% ) );
}
}
}
}
+ // Rule needs to be specific
+ // We want the expand button to appear outside the color
+ // to match the way the general highlight background appears
+ &-enhanced-grey td:not( :nth-child( -n+2 ) ) {
+ background-color: #dee0e3;
+ }
+
h4:first-of-type {
margin-top: 0;
padding-top: 0;
// and then plus the general margin
width: ~'calc( ( @{result-circle-diameter} + @{result-circle-margin} ) * 5 )';
// And we want to shift the entire block to the left of the li
- position: absolute;
- left: 0;
+ position: relative;
+ // Negative left margin of width + padding
+ margin-left: ~'calc( ( @{result-circle-diameter} + @{result-circle-margin} ) * -5 - @{result-circle-general-margin} )';
.mw-rcfilters-ui-changesListWrapperWidget-highlighted & {
display: inline-block;
.result-circle( c5 );
}
}
+}
- // One color
- .mw-rcfilters-highlight-color-c1 {
- background-color: tint( @highlight-c1, 70% );
- }
+// One color
+.mw-rcfilters-highlight-color-c1 {
+ .highlight-changesListWrapperWidget( tint( @highlight-c1, 70% ); );
+}
- .mw-rcfilters-highlight-color-c2 {
- background-color: tint( @highlight-c2, 70% );
- }
+.mw-rcfilters-highlight-color-c2 {
+ .highlight-changesListWrapperWidget( tint( @highlight-c2, 70% ); );
+}
- .mw-rcfilters-highlight-color-c3 {
- background-color: tint( @highlight-c3, 70% );
- }
+.mw-rcfilters-highlight-color-c3 {
+ .highlight-changesListWrapperWidget( tint( @highlight-c3, 70% ); );
+}
- .mw-rcfilters-highlight-color-c4 {
- background-color: tint( @highlight-c4, 70% );
- }
+.mw-rcfilters-highlight-color-c4 {
+ .highlight-changesListWrapperWidget( tint( @highlight-c4, 70% ); );
+}
- .mw-rcfilters-highlight-color-c5 {
- background-color: tint( @highlight-c5, 70% );
- }
+.mw-rcfilters-highlight-color-c5 {
+ .highlight-changesListWrapperWidget( tint( @highlight-c5, 70% ); );
+}
- // Two colors
- .highlight-color-mix( c1, c2 );
- // Overriding .highlight-color-mix( c1, c3 ); to produce
- // a custom color rather than the computed tint
- // see https://phabricator.wikimedia.org/T161267
- .mw-rcfilters-highlight-color-c1.mw-rcfilters-highlight-color-c3 {
- background-color: #ccdecc;
- }
- .highlight-color-mix( c1, c4 );
- .highlight-color-mix( c1, c5 );
- .highlight-color-mix( c2, c3 );
- .highlight-color-mix( c2, c4 );
- .highlight-color-mix( c2, c5 );
- .highlight-color-mix( c3, c4 );
- .highlight-color-mix( c3, c5 );
- .highlight-color-mix( c4, c5 );
-
- // Three colors
- .highlight-color-mix( c1, c2, c3 );
- .highlight-color-mix( c1, c2, c5 );
- .highlight-color-mix( c1, c2, c4 );
- .highlight-color-mix( c1, c3, c4 );
- .highlight-color-mix( c1, c3, c5 );
- .highlight-color-mix( c1, c4, c5 );
- .highlight-color-mix( c2, c3, c4 );
- .highlight-color-mix( c2, c3, c5 );
- .highlight-color-mix( c2, c4, c5 );
- .highlight-color-mix( c3, c4, c5 );
-
- // Four colors
- .highlight-color-mix( c1, c2, c3, c4 );
- .highlight-color-mix( c1, c2, c3, c5 );
- .highlight-color-mix( c1, c2, c4, c5 );
- .highlight-color-mix( c1, c3, c4, c5 );
- .highlight-color-mix( c2, c3, c4, c5 );
-
- // Five colors:
- .mw-rcfilters-highlight-color-c1.mw-rcfilters-highlight-color-c2.mw-rcfilters-highlight-color-c3.mw-rcfilters-highlight-color-c4.mw-rcfilters-highlight-color-c5 {
- background-color: tint( mix( @highlight-c1, mix( @highlight-c2, mix( @highlight-c3, average( @highlight-c4, @highlight-c5 ), 20% ), 20% ), 20% ), 15% );
- }
+// Two colors
+.highlight-color-mix( c1, c2 );
+// Overriding .highlight-color-mix( c1, c3 ); to produce
+// a custom color rather than the computed tint
+// see https://phabricator.wikimedia.org/T161267
+.mw-rcfilters-highlight-color-c1.mw-rcfilters-highlight-color-c3 {
+ .highlight-changesListWrapperWidget( #ccdecc );
+}
+.highlight-color-mix( c1, c4 );
+.highlight-color-mix( c1, c5 );
+.highlight-color-mix( c2, c3 );
+.highlight-color-mix( c2, c4 );
+.highlight-color-mix( c2, c5 );
+.highlight-color-mix( c3, c4 );
+.highlight-color-mix( c3, c5 );
+.highlight-color-mix( c4, c5 );
+
+// Three colors
+.highlight-color-mix( c1, c2, c3 );
+.highlight-color-mix( c1, c2, c5 );
+.highlight-color-mix( c1, c2, c4 );
+.highlight-color-mix( c1, c3, c4 );
+.highlight-color-mix( c1, c3, c5 );
+.highlight-color-mix( c1, c4, c5 );
+.highlight-color-mix( c2, c3, c4 );
+.highlight-color-mix( c2, c3, c5 );
+.highlight-color-mix( c2, c4, c5 );
+.highlight-color-mix( c3, c4, c5 );
+
+// Four colors
+.highlight-color-mix( c1, c2, c3, c4 );
+.highlight-color-mix( c1, c2, c3, c5 );
+.highlight-color-mix( c1, c2, c4, c5 );
+.highlight-color-mix( c1, c3, c4, c5 );
+.highlight-color-mix( c2, c3, c4, c5 );
+
+// Five colors:
+.mw-rcfilters-highlight-color-c1.mw-rcfilters-highlight-color-c2.mw-rcfilters-highlight-color-c3.mw-rcfilters-highlight-color-c4.mw-rcfilters-highlight-color-c5 {
+ .highlight-changesListWrapperWidget( tint( mix( @highlight-c1, mix( @highlight-c2, mix( @highlight-c3, average( @highlight-c4, @highlight-c5 ), 20% ), 20% ), 20% ), 15% ) );
}
margin-top: 1em;
}
- &-top {
- &-placeholder {
- width: 100%;
- }
-
- &-savedLinks {
- padding-left: 1em;
- vertical-align: bottom;
- }
- }
-
&-bottom {
margin-top: 1em;
--- /dev/null
+.mw-rcfilters-ui-rcTopSectionWidget {
+ &-topLinks {
+ &-table {
+ width: 100%;
+ }
+
+ &-top {
+ display: block;
+ width: 100%;
+
+ .mw-recentchanges-toplinks {
+ margin-bottom: 0.5em;
+ }
+ }
+ }
+
+ &-savedLinks {
+ vertical-align: bottom;
+ padding-left: 1em;
+ }
+
+ .mw-recentchanges-toplinks {
+ padding: 0 0.5em;
+
+ .oo-ui-widget-enabled.oo-ui-buttonElement.oo-ui-buttonElement-frameless .oo-ui-buttonElement-button {
+ padding: 0 2.5em 0 0.5em;
+ }
+
+ &-title,
+ .mw-collapsible-text {
+ // Same as the legend
+ font-size: 0.85em;
+ }
+
+ &:not( .mw-collapsed ) {
+ // Same as the legend
+ border: 1px solid #ddd;
+ }
+
+ &:not( .mw-rcfilters-ui-ready ) {
+ display: none;
+ }
+ }
+}
.mw-rcfilters-ui-savedLinksListWidget {
- float: right;
-
&-menu {
width: 100%;
}
--- /dev/null
+.mw-rcfilters-ui-watchlistTopSectionWidget {
+ &-watchlistDetails {
+ width: 100%;
+ }
+
+ &-editWatchlistButton {
+ vertical-align: bottom;
+
+ // actual button
+ .oo-ui-buttonWidget {
+ margin-left: 3em;
+ }
+ }
+
+ &-savedLinks {
+ float: right;
+ }
+
+ .mw-rcfilters-ui-table {
+ margin-top: 1em;
+ }
+
+ &-separator {
+ margin-top: 1em;
+ border-top: 2px solid #eaecf0; // Base80 AAA
+ }
+}
&-table {
display: table;
width: 100%;
+
+ &-placeholder {
+ width: 100%;
+ }
}
&-row {
this.filtersViewModel = filtersViewModel;
this.changesListViewModel = changesListViewModel;
this.controller = controller;
+ this.highlightClasses = null;
+ this.filtersModelInitialized = false;
// Events
this.filtersViewModel.connect( this, {
itemUpdate: 'onItemUpdate',
- highlightChange: 'onHighlightChange'
+ highlightChange: 'onHighlightChange',
+ initialize: 'onFiltersModelInitialize'
} );
this.changesListViewModel.connect( this, {
invalidate: 'onModelInvalidate',
// We handle our own display/hide of the empty results message
.removeClass( 'mw-changeslist-empty' );
- // Set up highlight containers
- this.setupHighlightContainers( this.$element );
-
this.setupNewChangesButtonContainer( this.$element );
};
OO.inheritClass( mw.rcfilters.ui.ChangesListWrapperWidget, OO.ui.Widget );
+ /**
+ * Respond to filters model initialize event
+ */
+ mw.rcfilters.ui.ChangesListWrapperWidget.prototype.onFiltersModelInitialize = function () {
+ this.filtersModelInitialized = true;
+ // Set up highlight containers. We need to wait for the filters model
+ // to be initialized, so we can make sure we have all the css class definitions
+ // we get from the server with our filters
+ this.setupHighlightContainers( this.$element );
+ };
+
+ /**
+ * Get all available highlight classes
+ *
+ * @return {string[]} An array of available highlight class names
+ */
+ mw.rcfilters.ui.ChangesListWrapperWidget.prototype.getHighlightClasses = function () {
+ if ( !this.highlightClasses || !this.highlightClasses.length ) {
+ this.highlightClasses = this.filtersViewModel.getItemsSupportingHighlights()
+ .map( function ( filterItem ) {
+ return filterItem.getCssClass();
+ } );
+ }
+
+ return this.highlightClasses;
+ };
+
/**
* Respond to the highlight feature being toggled on and off
*
* Respond to a filter item model update
*/
mw.rcfilters.ui.ChangesListWrapperWidget.prototype.onItemUpdate = function () {
- if ( this.filtersViewModel.isHighlightEnabled() ) {
+ if ( this.filtersModelInitialized && this.filtersViewModel.isHighlightEnabled() ) {
this.clearHighlight();
this.applyHighlight();
}
* @param {jQuery|string} $content The content of the updated changes list
*/
mw.rcfilters.ui.ChangesListWrapperWidget.prototype.setupHighlightContainers = function ( $content ) {
- var highlightClass = 'mw-rcfilters-ui-changesListWrapperWidget-highlights',
+ var $enhancedTopPageCell, $enhancedNestedPagesCell,
+ widget = this,
+ highlightClass = 'mw-rcfilters-ui-changesListWrapperWidget-highlights',
$highlights = $( '<div>' )
.addClass( highlightClass )
.append(
} );
if ( this.inEnhancedMode() ) {
- // Enhanced RC
- $content.find( 'td.mw-enhanced-rc' )
- .parent()
+ $enhancedTopPageCell = $content.find( 'table.mw-enhanced-rc.mw-collapsible' );
+ $enhancedNestedPagesCell = $content.find( 'td.mw-enhanced-rc-nested' );
+
+ // Enhanced RC highlight containers
+ $content.find( 'table.mw-enhanced-rc tr:first-child' )
+ .addClass( 'mw-rcfilters-ui-changesListWrapperWidget-enhanced-toplevel' )
.prepend(
$( '<td>' )
.append( $highlights.clone() )
);
+
+ // We are adding and changing cells in a table that, despite having nested rows,
+ // is actually all one big table. To do that right, we want to remove the 'placeholder'
+ // cell from the top row, because we're actually adding that placeholder in the children
+ // with the highlights.
+ $content.find( 'table.mw-enhanced-rc tr:first-child td.mw-changeslist-line-prefix' )
+ .detach();
+ $content.find( 'table.mw-enhanced-rc tr:first-child td.mw-enhanced-rc' )
+ .prop( 'colspan', '2' );
+
+ $enhancedNestedPagesCell
+ .before(
+ $( '<td>' )
+ .append( $highlights.clone().addClass( 'mw-enhanced-rc-nested' ) )
+ );
+
+ // We need to target the nested rows differently than the top rows so that the
+ // LESS rules applies correctly. In top rows, the rule should highlight all but
+ // the first 2 cells td:not( :nth-child( -n+2 ) and the nested rows, the rule
+ // should highlight all but the first 3 cells td:not( :nth-child( -n+3 )
+ $enhancedNestedPagesCell
+ .closest( 'tr' )
+ .addClass( 'mw-rcfilters-ui-changesListWrapperWidget-enhanced-nested' );
+
+ // Go over pages that have sub results
+ // HACK: We really only can collect those by targetting the collapsible class
+ $enhancedTopPageCell.each( function () {
+ var collectedClasses,
+ $table = $( this );
+
+ // Go over <tr>s and pick up all recognized classes
+ collectedClasses = widget.getHighlightClasses().filter( function ( className ) {
+ return $table.find( 'tr' ).hasClass( className );
+ } );
+
+ $table.find( 'tr:first-child' )
+ .addClass( collectedClasses.join( ' ' ) );
+ } );
+
+ $content.addClass( 'mw-rcfilters-ui-changesListWrapperWidget-enhancedView' );
} else {
// Regular RC
$content.find( 'ul.special li' )
}
};
+ /**
+ * In enhanced mode, we need to check whether the grouped results all have the
+ * same active highlights in order to see whether the "parent" of the group should
+ * be grey or highlighted normally.
+ *
+ * This is called every time highlights are applied.
+ */
+ mw.rcfilters.ui.ChangesListWrapperWidget.prototype.updateEnhancedParentHighlight = function () {
+ var activeHighlightClasses,
+ $enhancedTopPageCell = this.$element.find( 'table.mw-enhanced-rc.mw-collapsible' );
+
+ activeHighlightClasses = this.filtersViewModel.getCurrentlyUsedHighlightColors().map( function ( color ) {
+ return 'mw-rcfilters-highlight-color-' + color;
+ } );
+
+ // Go over top pages and their children, and figure out if all sub-pages have the
+ // same highlights between themselves. If they do, the parent should be highlighted
+ // with all colors. If classes are different, the parent should receive a grey
+ // background
+ $enhancedTopPageCell.each( function () {
+ var firstChildClasses, $rowsWithDifferentHighlights,
+ $table = $( this );
+
+ // Collect the relevant classes from the first nested child
+ firstChildClasses = activeHighlightClasses.filter( function ( className ) {
+ return $table.find( 'tr:nth-child(2)' ).hasClass( className );
+ } );
+ // Filter the non-head rows and see if they all have the same classes
+ // to the first row
+ $rowsWithDifferentHighlights = $table.find( 'tr:not(:first-child)' ).filter( function () {
+ var classesInThisRow,
+ $this = $( this );
+
+ classesInThisRow = activeHighlightClasses.filter( function ( className ) {
+ return $this.hasClass( className );
+ } );
+
+ return !OO.compare( firstChildClasses, classesInThisRow );
+ } );
+
+ // If classes are different, tag the row for using grey color
+ $table.find( 'tr:first-child' )
+ .toggleClass( 'mw-rcfilters-ui-changesListWrapperWidget-enhanced-grey', $rowsWithDifferentHighlights.length > 0 );
+ } );
+ };
+
/**
* @return {boolean} Whether the changes are grouped by page
*/
}
} );
+ if ( this.inEnhancedMode() ) {
+ this.updateEnhancedParentHighlight();
+ }
+
// Turn on highlights
this.$element.addClass( 'mw-rcfilters-ui-changesListWrapperWidget-highlighted' );
};
this.$element.find( '[data-highlightedFilters]' )
.removeAttr( 'title' )
.removeAttr( 'data-highlightedFilters' );
+
+ // Remove grey from enhanced rows
+ this.$element.find( '.mw-rcfilters-ui-changesListWrapperWidget-enhanced-grey' )
+ .removeClass( 'mw-rcfilters-ui-changesListWrapperWidget-enhanced-grey' );
+
// Turn off highlights
this.$element.removeClass( 'mw-rcfilters-ui-changesListWrapperWidget-highlighted' );
};
mw.rcfilters.ui.FilterWrapperWidget = function MwRcfiltersUiFilterWrapperWidget(
controller, model, savedQueriesModel, changesListModel, config
) {
- var $top, $bottom;
+ var $bottom;
config = config || {};
// Parent
);
// Initialize
- this.$topRow = $( '<div>' )
- .addClass( 'mw-rcfilters-ui-row' )
- .append(
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-cell' )
- .addClass( 'mw-rcfilters-ui-filterWrapperWidget-top-placeholder' )
- );
- $top = $( '<div>' )
- .addClass( 'mw-rcfilters-ui-filterWrapperWidget-top' )
- .addClass( 'mw-rcfilters-ui-table' )
- .append( this.$topRow );
+ this.$top = $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-filterWrapperWidget-top' );
$bottom = $( '<div>' )
.addClass( 'mw-rcfilters-ui-filterWrapperWidget-bottom' )
this.dateWidget.$element
);
- if ( !mw.user.isAnon() ) {
- this.savedLinksListWidget = new mw.rcfilters.ui.SavedLinksListWidget(
- this.controller,
- this.queriesModel,
- { $overlay: this.$overlay }
- );
-
- this.$topRow.append(
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-cell' )
- .addClass( 'mw-rcfilters-ui-filterWrapperWidget-top-savedLinks' )
- .append( this.savedLinksListWidget.$element )
- );
- }
-
if ( mw.rcfilters.featureFlags.liveUpdate ) {
$bottom.append( this.liveUpdateButton.$element );
}
this.$element
.addClass( 'mw-rcfilters-ui-filterWrapperWidget' )
.append(
- $top,
+ this.$top,
this.filterTagWidget.$element,
$bottom
);
/* Methods */
/**
- * Add a widget at the beginning of the top row
+ * Set the content of the top section
*
- * @param {OO.ui.Widget} widget Any widget
+ * @param {jQuery} $topSectionElement
*/
- mw.rcfilters.ui.FilterWrapperWidget.prototype.prependToTopRow = function ( widget ) {
- this.$topRow.prepend(
- widget.$element
- .addClass( 'mw-rcfilters-ui-cell' )
- );
+ mw.rcfilters.ui.FilterWrapperWidget.prototype.setTopSection = function ( $topSectionElement ) {
+ this.$top.append( $topSectionElement );
};
-
}( mediaWiki ) );
--- /dev/null
+( function ( mw ) {
+ /**
+ * Top section (between page title and filters) on Special:Recentchanges
+ *
+ * @extends OO.ui.Widget
+ *
+ * @constructor
+ * @param {mw.rcfilters.ui.SavedLinksListWidget} savedLinksListWidget
+ * @param {jQuery} $topLinks Content of the community-defined links
+ * @param {Object} [config] Configuration object
+ */
+ mw.rcfilters.ui.RcTopSectionWidget = function MwRcfiltersUiRcTopSectionWidget(
+ savedLinksListWidget, $topLinks, config
+ ) {
+ var toplinksTitle,
+ topLinksCookieName = 'rcfilters-toplinks-collapsed-state',
+ topLinksCookie = mw.cookie.get( topLinksCookieName ),
+ topLinksCookieValue = topLinksCookie || 'collapsed',
+ widget = this;
+
+ config = config || {};
+
+ // Parent
+ mw.rcfilters.ui.RcTopSectionWidget.parent.call( this, config );
+
+ this.$topLinks = $topLinks;
+
+ toplinksTitle = new OO.ui.ButtonWidget( {
+ framed: false,
+ indicator: topLinksCookieValue === 'collapsed' ? 'down' : 'up',
+ flags: [ 'progressive' ],
+ label: $( '<span>' ).append( mw.message( 'rcfilters-other-review-tools' ).parse() ).contents()
+ } );
+
+ this.$topLinks
+ .addClass( 'mw-rcfilters-ui-ready' )
+ .makeCollapsible( {
+ collapsed: topLinksCookieValue === 'collapsed',
+ $customTogglers: toplinksTitle.$element
+ } )
+ .on( 'beforeExpand.mw-collapsible', function () {
+ mw.cookie.set( topLinksCookieName, 'expanded' );
+ toplinksTitle.setIndicator( 'up' );
+ widget.switchTopLinks( 'expanded' );
+ } )
+ .on( 'beforeCollapse.mw-collapsible', function () {
+ mw.cookie.set( topLinksCookieName, 'collapsed' );
+ toplinksTitle.setIndicator( 'down' );
+ widget.switchTopLinks( 'collapsed' );
+ } );
+
+ this.$topLinks.find( '.mw-recentchanges-toplinks-title' ).replaceWith( toplinksTitle.$element );
+
+ // Create two positions for the toplinks to toggle between
+ // in the table (first cell) or up above it
+ this.$top = $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-rcTopSectionWidget-topLinks-top' );
+ this.$tableTopLinks = $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-cell' )
+ .addClass( 'mw-rcfilters-ui-rcTopSectionWidget-topLinks-table' );
+
+ // Initialize
+ this.$element
+ .addClass( 'mw-rcfilters-ui-rcTopSectionWidget' )
+ .append(
+ this.$top,
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-table' )
+ .append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-row' )
+ .append(
+ this.$tableTopLinks,
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-table-placeholder' )
+ .addClass( 'mw-rcfilters-ui-cell' ),
+ !mw.user.isAnon() ?
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-cell' )
+ .addClass( 'mw-rcfilters-ui-rcTopSectionWidget-savedLinks' )
+ .append( savedLinksListWidget.$element ) :
+ null
+ )
+ )
+ );
+
+ // Initialize top links position
+ widget.switchTopLinks( topLinksCookieValue );
+ };
+
+ /* Initialization */
+
+ OO.inheritClass( mw.rcfilters.ui.RcTopSectionWidget, OO.ui.Widget );
+
+ /**
+ * Switch the top links widget from inside the table (when collapsed)
+ * to the 'top' (when open)
+ *
+ * @param {string} [state] The state of the top links widget: 'expanded' or 'collapsed'
+ */
+ mw.rcfilters.ui.RcTopSectionWidget.prototype.switchTopLinks = function ( state ) {
+ state = state || 'expanded';
+
+ if ( state === 'expanded' ) {
+ this.$top.append( this.$topLinks );
+ } else {
+ this.$tableTopLinks.append( this.$topLinks );
+ }
+ };
+}( mediaWiki ) );
--- /dev/null
+( function ( mw ) {
+ /**
+ * Top section (between page title and filters) on Special:Watchlist
+ *
+ * @extends OO.ui.Widget
+ *
+ * @constructor
+ * @param {mw.rcfilters.Controller} controller
+ * @param {mw.rcfilters.dm.ChangesListViewModel} changesListModel
+ * @param {mw.rcfilters.ui.SavedLinksListWidget} savedLinksListWidget
+ * @param {jQuery} $watchlistDetails Content of the 'details' section that includes watched pages count
+ * @param {Object} [config] Configuration object
+ */
+ mw.rcfilters.ui.WatchlistTopSectionWidget = function MwRcfiltersUiWatchlistTopSectionWidget(
+ controller, changesListModel, savedLinksListWidget, $watchlistDetails, config
+ ) {
+ var editWatchlistButton,
+ markSeenButton,
+ $topTable,
+ $bottomTable,
+ $separator;
+ config = config || {};
+
+ // Parent
+ mw.rcfilters.ui.WatchlistTopSectionWidget.parent.call( this, config );
+
+ editWatchlistButton = new OO.ui.ButtonWidget( {
+ label: mw.msg( 'rcfilters-watchlist-editWatchlist-button' ),
+ icon: 'edit',
+ href: mw.config.get( 'wgStructuredChangeFiltersEditWatchlistUrl' )
+ } );
+ markSeenButton = new mw.rcfilters.ui.MarkSeenButtonWidget( controller, changesListModel );
+
+ $topTable = $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-table' )
+ .append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-row' )
+ .append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-cell' )
+ .addClass( 'mw-rcfilters-ui-watchlistTopSectionWidget-watchlistDetails' )
+ .append( $watchlistDetails )
+ )
+ .append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-cell' )
+ .addClass( 'mw-rcfilters-ui-watchlistTopSectionWidget-editWatchlistButton' )
+ .append( editWatchlistButton.$element )
+ )
+ );
+
+ $bottomTable = $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-table' )
+ .append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-row' )
+ .append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-cell' )
+ .append( markSeenButton.$element )
+ )
+ .append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-cell' )
+ .addClass( 'mw-rcfilters-ui-watchlistTopSectionWidget-savedLinks' )
+ .append( savedLinksListWidget.$element )
+ )
+ );
+
+ $separator = $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-watchlistTopSectionWidget-separator' );
+
+ this.$element
+ .addClass( 'mw-rcfilters-ui-watchlistTopSectionWidget' )
+ .append( $topTable, $separator, $bottomTable );
+ };
+
+ /* Initialization */
+
+ OO.inheritClass( mw.rcfilters.ui.WatchlistTopSectionWidget, OO.ui.Widget );
+}( mediaWiki ) );
button_italic.png
-------------------
-Source : http://commons.wikimedia.org/wiki/Image:Button_S_italic.png
+Source : https://commons.wikimedia.org/wiki/Image:Button_S_italic.png
License: Public domain
-Author : Purodha Blissenbach, http://ksh.wikipedia.org/wiki/User:Purodha
+Author : Purodha Blissenbach, https://ksh.wikipedia.org/wiki/User:Purodha
$this->assertArrayNotHasKey( "{$key}_id", $fields, "new field, stage=$writeStage" );
}
+ $extraFields[$pk] = $this->db->nextSequenceValue( "{$table}_{$pk}_seq" );
$this->db->insert( $table, $extraFields + $fields, __METHOD__ );
$id = $this->db->insertId();
if ( $usesTemp ) {
}
public static function provideInsertRoundTrip() {
+ $db = wfGetDB( DB_REPLICA ); // for timestamps
+
$msgComment = new Message( 'parentheses', [ 'message comment' ] );
$textCommentMsg = new RawMessage( '$1', [ 'text comment' ] );
$nestedMsgComment = new Message( [ 'parentheses', 'rawmessage' ], [ new Message( 'mainpage' ) ] );
$ipbfields = [
'ipb_range_start' => '',
'ipb_range_end' => '',
+ 'ipb_by' => 0,
+ 'ipb_timestamp' => $db->timestamp(),
+ 'ipb_expiry' => $db->getInfinity(),
];
$revfields = [
'rev_page' => 42,
'rev_text_id' => 42,
'rev_len' => 0,
+ 'rev_user' => 0,
+ 'rev_user_text' => '',
+ 'rev_timestamp' => $db->timestamp(),
];
$comStoreComment = new CommentStoreComment(
null, 'comment store comment', null, [ 'foo' => 'bar' ]