"wikimedia/html-formatter": "1.0.1",
"wikimedia/ip-set": "1.1.0",
"wikimedia/php-session-serializer": "1.0.4",
- "wikimedia/relpath": "1.0.3",
+ "wikimedia/relpath": "2.0.0",
"wikimedia/remex-html": "1.0.1",
"wikimedia/running-stat": "1.1.0",
"wikimedia/scoped-callback": "1.0.0",
$defaultPreferences['rcfilters-saved-queries'] = [
'type' => 'api',
];
+ $defaultPreferences['rcfilters-rclimit'] = [
+ 'type' => 'api',
+ ];
if ( $config->get( 'RCWatchCategoryMembership' ) ) {
$defaultPreferences['hidecategorization'] = [
[ 'addField', 'externallinks', 'el_index_60', 'patch-externallinks-el_index_60.sql' ],
[ 'dropIndex', 'oldimage', 'oi_name_archive_name',
'patch-alter-table-oldimage.sql' ],
+
+ // 1.30
+ [ 'modifyField', 'image', 'img_media_type', 'patch-add-3d.sql' ],
];
}
[ 'dropIndex', 'user_groups', 'ug_user_group', 'patch-user_groups-primary-key.sql' ],
[ 'addField', 'user_groups', 'ug_expiry', 'patch-user_groups-ug_expiry.sql' ],
[ 'addIndex', 'image', 'img_user_timestamp', 'patch-image-user-index-2.sql' ],
+
+ // 1.30
[ 'modifyField', 'image', 'img_media_type', 'patch-add-3d.sql' ],
];
}
[ 'addPgIndex', 'externallinks', 'el_from_index_60', '( el_from, el_index_60, el_id )' ],
[ 'addPgField', 'user_groups', 'ug_expiry', "TIMESTAMPTZ NULL" ],
[ 'addPgIndex', 'user_groups', 'user_groups_expiry', '( ug_expiry )' ],
+
+ // 1.30
+ [ 'modifyField', 'image', 'img_media_type', 'patch-add-3d.sql' ],
];
}
[ 'addField', 'externallinks', 'el_index_60', 'patch-externallinks-el_index_60.sql' ],
[ 'addField', 'user_groups', 'ug_expiry', 'patch-user_groups-ug_expiry.sql' ],
[ 'addIndex', 'image', 'img_user_timestamp', 'patch-image-user-index-2.sql' ],
+
+ // 1.30
+ [ 'modifyField', 'image', 'img_media_type', 'patch-add-3d.sql' ],
];
}
--- /dev/null
+ALTER TABLE /*$wgDBprefix*/image
+ DROP CONSTRAINT img_media_type_ckc;
+
+ALTER TABLE /*$wgDBprefix*/image
+ ADD CONSTRAINT img_media_type_ckc
+ CHECK (img_media_type IN("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE", "3D"));
+
+ALTER TABLE /*$wgDBprefix*/oldimage
+ DROP CONSTRAINT oi_media_type_ckc;
+
+ALTER TABLE /*$wgDBprefix*/oldimage
+ ADD CONSTRAINT oi_media_type_ckc
+ CHECK (oi_media_type IN("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE", "3D"));
+
+ALTER TABLE /*$wgDBprefix*/filearchive
+ DROP CONSTRAINT fa_media_type_ckc;
+
+ALTER TABLE /*$wgDBprefix*/filearchive
+ ADD CONSTRAINT fa_media_type_ckc
+ CHECK (fa_media_type IN("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE", "3D"));
+
+ALTER TABLE /*$wgDBprefix*/uploadstash
+ DROP CONSTRAINT us_media_type_ckc;
+
+ALTER TABLE /*$wgDBprefix*/uploadstash
+ ADD CONSTRAINT us_media_type_ckc
+ CHECK (us_media_type IN("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE", "3D"));
img_sha1 nvarchar(32) NOT NULL default '',
CONSTRAINT img_major_mime_ckc check (img_major_mime IN('unknown', 'application', 'audio', 'image', 'text', 'video', 'message', 'model', 'multipart', 'chemical')),
- CONSTRAINT img_media_type_ckc check (img_media_type in('UNKNOWN', 'BITMAP', 'DRAWING', 'AUDIO', 'VIDEO', 'MULTIMEDIA', 'OFFICE', 'TEXT', 'EXECUTABLE', 'ARCHIVE'))
+ CONSTRAINT img_media_type_ckc check (img_media_type in('UNKNOWN', 'BITMAP', 'DRAWING', 'AUDIO', 'VIDEO', 'MULTIMEDIA', 'OFFICE', 'TEXT', 'EXECUTABLE', 'ARCHIVE','3D'))
);
CREATE INDEX /*i*/img_usertext_timestamp ON /*_*/image (img_user_text,img_timestamp);
oi_sha1 nvarchar(32) NOT NULL default '',
CONSTRAINT oi_major_mime_ckc check (oi_major_mime IN('unknown', 'application', 'audio', 'image', 'text', 'video', 'message', 'model', 'multipart', 'chemical')),
- CONSTRAINT oi_media_type_ckc check (oi_media_type IN('UNKNOWN', 'BITMAP', 'DRAWING', 'AUDIO', 'VIDEO', 'MULTIMEDIA', 'OFFICE', 'TEXT', 'EXECUTABLE', 'ARCHIVE'))
+ CONSTRAINT oi_media_type_ckc check (oi_media_type IN('UNKNOWN', 'BITMAP', 'DRAWING', 'AUDIO', 'VIDEO', 'MULTIMEDIA', 'OFFICE', 'TEXT', 'EXECUTABLE', 'ARCHIVE','3D'))
);
CREATE INDEX /*i*/oi_usertext_timestamp ON /*_*/oldimage (oi_user_text,oi_timestamp);
fa_sha1 nvarchar(32) NOT NULL default '',
CONSTRAINT fa_major_mime_ckc check (fa_major_mime in('unknown', 'application', 'audio', 'image', 'text', 'video', 'message', 'model', 'multipart', 'chemical')),
- CONSTRAINT fa_media_type_ckc check (fa_media_type in('UNKNOWN', 'BITMAP', 'DRAWING', 'AUDIO', 'VIDEO', 'MULTIMEDIA', 'OFFICE', 'TEXT', 'EXECUTABLE', 'ARCHIVE'))
+ CONSTRAINT fa_media_type_ckc check (fa_media_type in('UNKNOWN', 'BITMAP', 'DRAWING', 'AUDIO', 'VIDEO', 'MULTIMEDIA', 'OFFICE', 'TEXT', 'EXECUTABLE', 'ARCHIVE','3D'))
);
-- pick out by image name
us_image_height int,
us_image_bits smallint,
- CONSTRAINT us_media_type_ckc check (us_media_type in('UNKNOWN', 'BITMAP', 'DRAWING', 'AUDIO', 'VIDEO', 'MULTIMEDIA', 'OFFICE', 'TEXT', 'EXECUTABLE', 'ARCHIVE'))
+ CONSTRAINT us_media_type_ckc check (us_media_type in('UNKNOWN', 'BITMAP', 'DRAWING', 'AUDIO', 'VIDEO', 'MULTIMEDIA', 'OFFICE', 'TEXT', 'EXECUTABLE', 'ARCHIVE', '3D'))
);
-- sometimes there's a delete for all of a user's stuff.
--- /dev/null
+ALTER TYPE media_type ADD VALUE '3D';
CREATE INDEX fa_sha1 ON filearchive (fa_sha1);
CREATE SEQUENCE uploadstash_us_id_seq;
-CREATE TYPE media_type AS ENUM ('UNKNOWN','BITMAP','DRAWING','AUDIO','VIDEO','MULTIMEDIA','OFFICE','TEXT','EXECUTABLE','ARCHIVE');
+CREATE TYPE media_type AS ENUM ('UNKNOWN','BITMAP','DRAWING','AUDIO','VIDEO','MULTIMEDIA','OFFICE','TEXT','EXECUTABLE','ARCHIVE','3D');
CREATE TABLE uploadstash (
us_id INTEGER PRIMARY KEY NOT NULL DEFAULT nextval('uploadstash_us_id_seq'),
--- /dev/null
+-- image
+
+CREATE TABLE /*_*/image_tmp (
+ -- Filename.
+ -- This is also the title of the associated description page,
+ -- which will be in namespace 6 (NS_FILE).
+ img_name varchar(255) binary NOT NULL default '' PRIMARY KEY,
+
+ -- File size in bytes.
+ img_size int unsigned NOT NULL default 0,
+
+ -- For images, size in pixels.
+ img_width int NOT NULL default 0,
+ img_height int NOT NULL default 0,
+
+ -- Extracted Exif metadata stored as a serialized PHP array.
+ img_metadata mediumblob NOT NULL,
+
+ -- For images, bits per pixel if known.
+ img_bits int NOT NULL default 0,
+
+ -- Media type as defined by the MEDIATYPE_xxx constants
+ img_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE", "3D") default NULL,
+
+ -- major part of a MIME media type as defined by IANA
+ -- see https://www.iana.org/assignments/media-types/
+ -- for "chemical" cf. http://dx.doi.org/10.1021/ci9803233 by the ACS
+ img_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart", "chemical") NOT NULL default "unknown",
+
+ -- minor part of a MIME media type as defined by IANA
+ -- the minor parts are not required to adher to any standard
+ -- but should be consistent throughout the database
+ -- see https://www.iana.org/assignments/media-types/
+ img_minor_mime varbinary(100) NOT NULL default "unknown",
+
+ -- Description field as entered by the uploader.
+ -- This is displayed in image upload history and logs.
+ img_description varbinary(767) NOT NULL,
+
+ -- user_id and user_name of uploader.
+ img_user int unsigned NOT NULL default 0,
+ img_user_text varchar(255) binary NOT NULL,
+
+ -- Time of the upload.
+ img_timestamp varbinary(14) NOT NULL default '',
+
+ -- SHA-1 content hash in base-36
+ img_sha1 varbinary(32) NOT NULL default ''
+) /*$wgDBTableOptions*/;
+
+INSERT INTO /*_*/image_tmp
+ SELECT img_name, img_size, img_width, img_height, img_metadata, img_bits,
+ img_media_type, img_major_mime, img_minor_mime, img_description,
+ img_user, img_user_text, img_timestamp, img_sha1
+ FROM /*_*/image;
+
+DROP TABLE /*_*/image;
+
+ALTER TABLE /*_*/image_tmp RENAME TO /*_*/image;
+
+-- Used by Special:Newimages and ApiQueryAllImages
+CREATE INDEX /*i*/img_user_timestamp ON /*_*/image (img_user,img_timestamp);
+CREATE INDEX /*i*/img_usertext_timestamp ON /*_*/image (img_user_text,img_timestamp);
+-- Used by Special:ListFiles for sort-by-size
+CREATE INDEX /*i*/img_size ON /*_*/image (img_size);
+-- Used by Special:Newimages and Special:ListFiles
+CREATE INDEX /*i*/img_timestamp ON /*_*/image (img_timestamp);
+-- Used in API and duplicate search
+CREATE INDEX /*i*/img_sha1 ON /*_*/image (img_sha1(10));
+-- Used to get media of one type
+CREATE INDEX /*i*/img_media_mime ON /*_*/image (img_media_type,img_major_mime,img_minor_mime);
+
+-- oldimage
+
+CREATE TABLE /*_*/oldimage_tmp (
+ -- Base filename: key to image.img_name
+ oi_name varchar(255) binary NOT NULL default '',
+
+ -- Filename of the archived file.
+ -- This is generally a timestamp and '!' prepended to the base name.
+ oi_archive_name varchar(255) binary NOT NULL default '',
+
+ -- Other fields as in image...
+ oi_size int unsigned NOT NULL default 0,
+ oi_width int NOT NULL default 0,
+ oi_height int NOT NULL default 0,
+ oi_bits int NOT NULL default 0,
+ oi_description varbinary(767) NOT NULL,
+ oi_user int unsigned NOT NULL default 0,
+ oi_user_text varchar(255) binary NOT NULL,
+ oi_timestamp binary(14) NOT NULL default '',
+
+ oi_metadata mediumblob NOT NULL,
+ oi_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE", "3D") default NULL,
+ oi_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart", "chemical") NOT NULL default "unknown",
+ oi_minor_mime varbinary(100) NOT NULL default "unknown",
+ oi_deleted tinyint unsigned NOT NULL default 0,
+ oi_sha1 varbinary(32) NOT NULL default ''
+) /*$wgDBTableOptions*/;
+
+INSERT INTO /*_*/oldimage_tmp
+ SELECT oi_name, oi_archive_name, oi_size, oi_width, oi_height, oi_bits,
+ oi_description, oi_user, oi_user_text, oi_timestamp, oi_metadata,
+ oi_media_type, oi_major_mime, oi_minor_mime, oi_deleted, oi_sha1
+ FROM /*_*/oldimage;
+
+DROP TABLE /*_*/oldimage;
+
+ALTER TABLE oldimage_tmp RENAME TO /*_*/oldimage;
+
+CREATE INDEX /*i*/oi_usertext_timestamp ON /*_*/oldimage (oi_user_text,oi_timestamp);
+CREATE INDEX /*i*/oi_name_timestamp ON /*_*/oldimage (oi_name,oi_timestamp);
+-- oi_archive_name truncated to 14 to avoid key length overflow
+CREATE INDEX /*i*/oi_name_archive_name ON /*_*/oldimage (oi_name,oi_archive_name(14));
+CREATE INDEX /*i*/oi_sha1 ON /*_*/oldimage (oi_sha1(10));
+
+-- filearchive
+
+CREATE TABLE /*_*/filearchive_tmp (
+ -- Unique row id
+ fa_id int NOT NULL PRIMARY KEY AUTO_INCREMENT,
+
+ -- Original base filename; key to image.img_name, page.page_title, etc
+ fa_name varchar(255) binary NOT NULL default '',
+
+ -- Filename of archived file, if an old revision
+ fa_archive_name varchar(255) binary default '',
+
+ -- Which storage bin (directory tree or object store) the file data
+ -- is stored in. Should be 'deleted' for files that have been deleted;
+ -- any other bin is not yet in use.
+ fa_storage_group varbinary(16),
+
+ -- SHA-1 of the file contents plus extension, used as a key for storage.
+ -- eg 8f8a562add37052a1848ff7771a2c515db94baa9.jpg
+ --
+ -- If NULL, the file was missing at deletion time or has been purged
+ -- from the archival storage.
+ fa_storage_key varbinary(64) default '',
+
+ -- Deletion information, if this file is deleted.
+ fa_deleted_user int,
+ fa_deleted_timestamp binary(14) default '',
+ fa_deleted_reason varbinary(767) default '',
+
+ -- Duped fields from image
+ fa_size int unsigned default 0,
+ fa_width int default 0,
+ fa_height int default 0,
+ fa_metadata mediumblob,
+ fa_bits int default 0,
+ fa_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE", "3D") default NULL,
+ fa_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart", "chemical") default "unknown",
+ fa_minor_mime varbinary(100) default "unknown",
+ fa_description varbinary(767),
+ fa_user int unsigned default 0,
+ fa_user_text varchar(255) binary,
+ fa_timestamp binary(14) default '',
+
+ -- Visibility of deleted revisions, bitfield
+ fa_deleted tinyint unsigned NOT NULL default 0,
+
+ -- sha1 hash of file content
+ fa_sha1 varbinary(32) NOT NULL default ''
+) /*$wgDBTableOptions*/;
+
+INSERT INTO /*_*/filearchive_tmp
+ SELECT fa_id, fa_name, fa_archive_name, fa_storage_group, fa_storage_key, fa_deleted_user, fa_deleted_timestamp,
+ fa_deleted_reason, fa_size, fa_width, fa_height, fa_metadata, fa_bits, fa_media_type, fa_major_mime,
+ fa_minor_mime, fa_description, fa_user, fa_user_text, fa_timestamp, fa_deleted, fa_sha1
+ FROM /*_*/filearchive;
+
+DROP TABLE /*_*/filearchive;
+
+ALTER TABLE /*_*/filearchive_tmp RENAME TO /*_*/filearchive;
+
+-- pick out by image name
+CREATE INDEX /*i*/fa_name ON /*_*/filearchive (fa_name, fa_timestamp);
+-- pick out dupe files
+CREATE INDEX /*i*/fa_storage_group ON /*_*/filearchive (fa_storage_group, fa_storage_key);
+-- sort by deletion time
+CREATE INDEX /*i*/fa_deleted_timestamp ON /*_*/filearchive (fa_deleted_timestamp);
+-- sort by uploader
+CREATE INDEX /*i*/fa_user_timestamp ON /*_*/filearchive (fa_user_text,fa_timestamp);
+-- find file by sha1, 10 bytes will be enough for hashes to be indexed
+CREATE INDEX /*i*/fa_sha1 ON /*_*/filearchive (fa_sha1(10));
+
+-- uploadstash
+
+CREATE TABLE /*_*/uploadstash_tmp (
+ us_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
+
+ -- the user who uploaded the file.
+ us_user int unsigned NOT NULL,
+
+ -- file key. this is how applications actually search for the file.
+ -- this might go away, or become the primary key.
+ us_key varchar(255) NOT NULL,
+
+ -- the original path
+ us_orig_path varchar(255) NOT NULL,
+
+ -- the temporary path at which the file is actually stored
+ us_path varchar(255) NOT NULL,
+
+ -- which type of upload the file came from (sometimes)
+ us_source_type varchar(50),
+
+ -- the date/time on which the file was added
+ us_timestamp varbinary(14) NOT NULL,
+
+ us_status varchar(50) NOT NULL,
+
+ -- chunk counter starts at 0, current offset is stored in us_size
+ us_chunk_inx int unsigned NULL,
+
+ -- Serialized file properties from FSFile::getProps()
+ us_props blob,
+
+ -- file size in bytes
+ us_size int unsigned NOT NULL,
+ -- this hash comes from FSFile::getSha1Base36(), and is 31 characters
+ us_sha1 varchar(31) NOT NULL,
+ us_mime varchar(255),
+ -- Media type as defined by the MEDIATYPE_xxx constants, should duplicate definition in the image table
+ us_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE", "3D") default NULL,
+ -- image-specific properties
+ us_image_width int unsigned,
+ us_image_height int unsigned,
+ us_image_bits smallint unsigned
+
+) /*$wgDBTableOptions*/;
+
+INSERT INTO /*_*/uploadstash_tmp
+ SELECT us_id, us_user, us_key, us_orig_path, us_path, us_source_type,
+ us_timestamp, us_status, us_chunk_inx, us_props, us_size, us_sha1, us_mime,
+ us_media_type, us_image_width, us_image_height, us_image_bits
+ FROM /*_*/uploadstash;
+
+DROP TABLE uploadstash;
+
+ALTER TABLE /*_*/uploadstash_tmp RENAME TO /*_*/uploadstash;
+
+-- sometimes there's a delete for all of a user's stuff.
+CREATE INDEX /*i*/us_user ON /*_*/uploadstash (us_user);
+-- pick out files by key, enforce key uniqueness
+CREATE UNIQUE INDEX /*i*/us_key ON /*_*/uploadstash (us_key);
+-- the abandoned upload cleanup script needs this
+CREATE INDEX /*i*/us_timestamp ON /*_*/uploadstash (us_timestamp);
* @cfg {string} [type='send_unselected_if_any'] Group type
* @cfg {string} [view='default'] Name of the display group this group
* is a part of.
+ * @cfg {boolean} [isSticky] This group is using a 'sticky' default; meaning
+ * that every time a value is changed, it becomes the new default
* @cfg {string} [title] Group title
* @cfg {boolean} [hidden] This group is hidden from the regular menu views
* @cfg {boolean} [allowArbitrary] Allows for an arbitrary value to be added to the
this.name = name;
this.type = config.type || 'send_unselected_if_any';
this.view = config.view || 'default';
+ this.sticky = !!config.isSticky;
this.title = config.title || name;
this.hidden = !!config.hidden;
this.allowArbitrary = !!config.allowArbitrary;
var subsetNames = [],
filterItem = new mw.rcfilters.dm.FilterItem( filter.name, model, {
group: model.getName(),
- useDefaultAsBaseValue: !!filter.useDefaultAsBaseValue,
label: filter.label || filter.name,
description: filter.description || '',
labelPrefixKey: model.labelPrefixKey,
// For this group, the parameter is the group name,
// and a single item can be selected: default or first item
this.defaultParams[ this.getName() ] = defaultParam;
-
- // Single option means there must be a single option
- // selected, so we have to either select the default
- // or select the first option
- this.selectItemByParamName( defaultParam );
}
// Store default filter state based on default params
this.defaultFilters = this.getFilterRepresentation( this.getDefaultParams() );
// Check for filters that should be initially selected by their default value
- this.getItems().forEach( function ( item ) {
- if (
- item.isUsingDefaultAsBaseValue() &&
- (
- // This setting can only be applied to these groups
- // the other groups are way too complex for that
- model.getType() === 'single_option' ||
- model.getType() === 'boolean'
- )
- ) {
- // Apply selection
- item.toggleSelected( !!model.defaultFilters[ item.getName() ] );
- }
- } );
+ if ( this.isSticky() ) {
+ $.each( this.defaultFilters, function ( filterName, filterValue ) {
+ model.getItemByName( filterName ).toggleSelected( filterValue );
+ } );
+ }
+
+ // Verify that single_option group has at least one item selected
+ if (
+ this.getType() === 'single_option' &&
+ this.getSelectedItems().length === 0
+ ) {
+ defaultParam = groupDefault !== undefined ?
+ groupDefault : this.getItems()[ 0 ].getParamName();
+
+ // Single option means there must be a single option
+ // selected, so we have to either select the default
+ // or select the first option
+ this.selectItemByParamName( defaultParam );
+ }
};
/**
// Single option means there must be a single option
// selected, so we have to either select the default
// or select the first option
- this.currSelected = this.getItemByParamName( this.defaultParams[ this.getName() ] );
+ this.currSelected = this.getItemByParamName( this.defaultParams[ this.getName() ] ) ||
+ this.getItems()[ 0 ];
this.currSelected.toggleSelected( true );
changed = true;
}
this.active !== active ||
this.currSelected !== item
) {
+ if ( this.isSticky() ) {
+ // If this group is sticky, then change the default according to the
+ // current selection.
+ this.defaultParams = this.getParamRepresentation( this.getSelectedState() );
+ }
+
this.active = active;
this.currSelected = item;
// This means we have not been given a filter representation
// so we are building one based on current state
filterRepresentation[ item.getName() ] = item.isSelected();
- } else if ( !filterRepresentation[ item.getName() ] ) {
+ } else if ( filterRepresentation[ item.getName() ] === undefined ) {
// We are given a filter representation, but we have to make
// sure that we fill in the missing filters if there are any
- // we will assume they are all falsey, unless they have
- // isUsingDefaultAsBaseValue, in which case they get their
- // default state
- if (
- item.isUsingDefaultAsBaseValue() &&
- (
- // This setting can only be applied to these groups
- // the other groups are way too complex for that
- model.getType() === 'single_option' ||
- model.getType() === 'boolean'
- )
- ) {
+ // we will assume they are all falsey
+ if ( model.isSticky() ) {
filterRepresentation[ item.getName() ] = !!defaultFilters[ item.getName() ];
} else {
filterRepresentation[ item.getName() ] = false;
* @return {Object} Filter representation
*/
mw.rcfilters.dm.FilterGroup.prototype.getFilterRepresentation = function ( paramRepresentation ) {
- var areAnySelected, paramValues, defaultValue, item, currentValue,
+ var areAnySelected, paramValues, item, currentValue,
oneWasSelected = false,
defaultParams = this.getDefaultParams(),
- defaultFilters = this.getDefaultFilters(),
expandedParams = $.extend( true, {}, paramRepresentation ),
model = this,
paramToFilterMap = {},
result = {};
+ if ( this.isSticky() ) {
+ // If the group is sticky, check if all parameters are represented
+ // and for those that aren't represented, add them with their default
+ // values
+ paramRepresentation = $.extend( true, {}, this.getDefaultParams(), paramRepresentation );
+ }
+
paramRepresentation = paramRepresentation || {};
if (
this.getType() === 'send_unselected_if_any' ||
} );
$.each( expandedParams, function ( paramName, paramValue ) {
- var value = paramValue,
- filterItem = paramToFilterMap[ paramName ];
+ var filterItem = paramToFilterMap[ paramName ];
if ( model.getType() === 'send_unselected_if_any' ) {
// Flip the definition between the parameter
false;
} else if ( model.getType() === 'boolean' ) {
// Straight-forward definition of state
- if (
- filterItem.isUsingDefaultAsBaseValue() &&
- paramRepresentation[ filterItem.getParamName() ] === undefined
- ) {
- value = defaultParams[ filterItem.getParamName() ];
- }
- result[ filterItem.getName() ] = !!Number( value );
+ result[ filterItem.getName() ] = !!Number( paramRepresentation[ filterItem.getParamName() ] );
}
} );
} else if ( this.getType() === 'string_options' ) {
} else if ( this.getType() === 'single_option' ) {
// There is parameter that fits a single filter and if not, get the default
this.getItems().forEach( function ( filterItem ) {
- var selected = false;
+ var selected = filterItem.getParamName() === paramRepresentation[ model.getName() ];
- if (
- filterItem.isUsingDefaultAsBaseValue() &&
- paramRepresentation[ model.getName() ] === undefined
- ) {
- selected = !!Number( paramRepresentation[ model.getName() ] );
- } else {
- selected = filterItem.getParamName() === paramRepresentation[ model.getName() ];
- }
result[ filterItem.getName() ] = selected;
oneWasSelected = oneWasSelected || selected;
} );
// Go over result and make sure all filters are represented.
// If any filters are missing, they will get a falsey value
this.getItems().forEach( function ( filterItem ) {
- if (
- (
- // This setting can only be applied to these groups
- // the other groups are way too complex for that
- model.getType() === 'single_option' ||
- model.getType() === 'boolean'
- ) &&
- result[ filterItem.getName() ] === undefined &&
- filterItem.isUsingDefaultAsBaseValue()
- ) {
- result[ filterItem.getName() ] = !!defaultFilters[ filterItem.getName() ];
+ if ( result[ filterItem.getName() ] === undefined ) {
+ result[ filterItem.getName() ] = false;
}
- oneWasSelected = oneWasSelected || !!result[ filterItem.getName() ];
} );
// Make sure that at least one option is selected in
this.getType() === 'single_option' &&
!oneWasSelected
) {
- defaultValue = this.getDefaultParams();
- item = this.getItemByParamName( defaultValue[ this.getName() ] );
+ if ( defaultParams[ this.getName() ] ) {
+ item = this.getItemByParamName( defaultParams[ this.getName() ] );
+ } else {
+ item = this.getItems()[ 0 ];
+ }
result[ item.getName() ] = true;
}
return result;
};
+ /**
+ * Get current selected state of all filter items in this group
+ *
+ * @return {Object} Selected state
+ */
+ mw.rcfilters.dm.FilterGroup.prototype.getSelectedState = function () {
+ var state = {};
+
+ this.getItems().forEach( function ( filterItem ) {
+ state[ filterItem.getName() ] = filterItem.isSelected();
+ } );
+
+ return state;
+ };
+
/**
* Get item by its filter name
*
mw.rcfilters.dm.FilterGroup.prototype.isFullCoverage = function () {
return this.fullCoverage;
};
+
+ /**
+ * Check whether the group is defined as sticky default
+ *
+ * @return {boolean} Group is sticky default
+ */
+ mw.rcfilters.dm.FilterGroup.prototype.isSticky = function () {
+ return this.sticky;
+ };
}( mediaWiki ) );
return result;
};
+ /**
+ * Get a parameter representation of all sticky parameters
+ *
+ * @return {Object} Sticky parameter values
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getStickyParams = function () {
+ var result = {};
+
+ $.each( this.groups, function ( name, model ) {
+ if ( model.isSticky() ) {
+ $.extend( true, result, model.getDefaultParams() );
+ }
+ } );
+
+ return result;
+ };
+
+ /**
+ * Get a filter representation of all sticky parameters
+ *
+ * @return {Object} Sticky filters values
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getStickyFiltersState = function () {
+ var result = {};
+
+ $.each( this.groups, function ( name, model ) {
+ if ( model.isSticky() ) {
+ $.extend( true, result, model.getSelectedState() );
+ }
+ } );
+
+ return result;
+ };
+
/**
* Analyze the groups and their filters and output an object representing
* the state of the parameters they represent.
this.namePrefix = config.namePrefix || 'item_';
this.name = this.namePrefix + param;
- this.useDefaultAsBaseValue = !!config.useDefaultAsBaseValue;
this.label = config.label || this.name;
this.labelPrefixKey = config.labelPrefixKey;
this.description = config.description || '';
return this.identifiers;
};
- /**
- * Check whether the item uses its default state as a base value
- *
- * @return {boolean} Use default as base value
- */
- mw.rcfilters.dm.ItemModel.prototype.isUsingDefaultAsBaseValue = function () {
- return this.useDefaultAsBaseValue;
- };
-
/**
* Toggle the highlight feature on and off for this filter.
* It only works if highlight is supported for this filter.
* the above structure.
* @param {Object} [baseState] An object representing the base state
* so we can normalize the data
+ * @param {string[]} [ignoreFilters] Filters to ignore and remove from
+ * the data
* @fires initialize
*/
- mw.rcfilters.dm.SavedQueriesModel.prototype.initialize = function ( savedQueries, baseState ) {
+ mw.rcfilters.dm.SavedQueriesModel.prototype.initialize = function ( savedQueries, baseState, ignoreFilters ) {
var items = [],
defaultItem = null;
savedQueries = savedQueries || {};
+ ignoreFilters = ignoreFilters || {};
this.baseState = baseState;
// for existing users, who are only betalabs users at the moment.
normalizedData.highlights.highlight = !!Number( normalizedData.highlights.highlight );
+ // Backwards-compat fix: Remove sticky parameters from the 'ignoreFilters' list
+ ignoreFilters.forEach( function ( name ) {
+ delete normalizedData.filters[ name ];
+ } );
+
item = new mw.rcfilters.dm.SavedQueryItemModel(
id,
obj.label,
* @param {Object} [tagList] Tag definition
*/
mw.rcfilters.Controller.prototype.initialize = function ( filterStructure, namespaceStructure, tagList ) {
- var parsedSavedQueries,
+ var parsedSavedQueries, limitDefault,
controller = this,
views = {},
items = [],
};
}
+ // Convert the default from the old preference
+ // since the limit preference actually affects more
+ // than just the RecentChanges page
+ limitDefault = Number( mw.user.options.get( 'rcfilters-rclimit', mw.user.options.get( 'rclimit', '50' ) ) );
+
// Add parameter range operations
views.range = {
groups: [
allowArbitrary: true,
validate: $.isNumeric,
sortFunc: function ( a, b ) { return Number( a.name ) - Number( b.name ); },
- 'default': mw.user.options.get( 'rclimit' ),
+ 'default': String( limitDefault ),
+ isSticky: true,
filters: [ 50, 100, 250, 500 ].map( function ( num ) {
return controller._createFilterDataFromNumber( num, num );
} )
( Number( i ) * 24 ).toFixed( 2 ) :
Number( i );
},
- 'default': mw.user.options.get( 'rcdays' ),
+ 'default': mw.user.options.get( 'rcdays', '30' ),
+ isSticky: true,
filters: [
// Hours (1, 2, 6, 12)
0.04166, 0.0833, 0.25, 0.5,
// can normalize them per each query item
this.savedQueriesModel.initialize(
parsedSavedQueries,
- this._getBaseFilterState()
+ this._getBaseFilterState(),
+ // This is for backwards compatibility - delete all sticky filter states
+ Object.keys( this.filtersModel.getStickyFiltersState() )
);
// Check whether we need to load defaults.
*/
mw.rcfilters.Controller.prototype.resetToDefaults = function () {
this.uriProcessor.updateModelBasedOnQuery( this._getDefaultParams() );
+
this.updateChangesList();
};
mw.rcfilters.Controller.prototype.saveCurrentQuery = function ( label, setAsDefault ) {
var queryID,
highlightedItems = {},
- highlightEnabled = this.filtersModel.isHighlightEnabled();
+ highlightEnabled = this.filtersModel.isHighlightEnabled(),
+ selectedState = this.filtersModel.getSelectedState();
// Prepare highlights
this.filtersModel.getHighlightedItems().forEach( function ( item ) {
// These are filter states; highlight is stored as boolean
highlightedItems.highlight = this.filtersModel.isHighlightEnabled();
+ // Delete all sticky filters
+ this._deleteStickyValuesFromFilterState( selectedState );
+
// Add item
queryID = this.savedQueriesModel.addNewQuery(
label || mw.msg( 'rcfilters-savedqueries-defaultlabel' ),
{
- filters: this.filtersModel.getSelectedState(),
+ filters: selectedState,
highlights: highlightedItems,
invert: this.filtersModel.areNamespacesInverted()
}
highlights.highlight = highlights.highlights || highlights.highlight;
// Update model state from filters
- this.filtersModel.toggleFiltersSelected( data.filters );
+ this.filtersModel.toggleFiltersSelected(
+ // Merge filters with sticky values
+ $.extend( true, {}, data.filters, this.filtersModel.getStickyFiltersState() )
+ );
// Update namespace inverted property
this.filtersModel.toggleInvertedNamespaces( !!Number( data.invert ) );
* @return {boolean} Query exists
*/
mw.rcfilters.Controller.prototype.findQueryMatchingCurrentState = function () {
- var highlightedItems = {};
+ var highlightedItems = {},
+ selectedState = this.filtersModel.getSelectedState();
// Prepare highlights of the current query
this.filtersModel.getItemsSupportingHighlights().forEach( function ( item ) {
} );
highlightedItems.highlight = this.filtersModel.isHighlightEnabled();
+ // Remove sticky filters
+ this._deleteStickyValuesFromFilterState( selectedState );
+
return this.savedQueriesModel.findMatchingQuery(
{
- filters: this.filtersModel.getSelectedState(),
+ filters: selectedState,
highlights: highlightedItems,
invert: this.filtersModel.areNamespacesInverted()
}
);
};
+ /**
+ * Delete sticky filters from given object
+ *
+ * @param {Object} filterState Filter state
+ */
+ mw.rcfilters.Controller.prototype._deleteStickyValuesFromFilterState = function ( filterState ) {
+ // Remove sticky filters
+ $.each( this.filtersModel.getStickyFiltersState(), function ( filterName ) {
+ delete filterState[ filterName ];
+ } );
+ };
+
/**
* Get an object representing the base state of parameters
* and highlights.
mw.user.options.set( 'rcfilters-saved-queries', stringified );
};
+ /**
+ * Update sticky preferences with current model state
+ */
+ mw.rcfilters.Controller.prototype.updateStickyPreferences = function () {
+ // Update default sticky values with selected, whether they came from
+ // the initial defaults or from the URL value that is being normalized
+ this.updateDaysDefault( this.filtersModel.getGroup( 'days' ).getSelectedItems()[ 0 ].getParamName() );
+ this.updateLimitDefault( this.filtersModel.getGroup( 'limit' ).getSelectedItems()[ 0 ].getParamName() );
+ };
+
+ /**
+ * Update the limit default value
+ *
+ * @param {number} newValue New value
+ */
+ mw.rcfilters.Controller.prototype.updateLimitDefault = function ( newValue ) {
+ if ( !$.isNumeric( newValue ) ) {
+ return;
+ }
+
+ newValue = Number( newValue );
+
+ if ( mw.user.options.get( 'rcfilters-rclimit' ) !== newValue ) {
+ // Save the preference
+ new mw.Api().saveOption( 'rcfilters-rclimit', newValue );
+ // Update the preference for this session
+ mw.user.options.set( 'rcfilters-rclimit', newValue );
+ }
+ };
+
+ /**
+ * Update the days default value
+ *
+ * @param {number} newValue New value
+ */
+ mw.rcfilters.Controller.prototype.updateDaysDefault = function ( newValue ) {
+ if ( !$.isNumeric( newValue ) ) {
+ return;
+ }
+
+ newValue = Number( newValue );
+
+ if ( mw.user.options.get( 'rcdays' ) !== newValue ) {
+ // Save the preference
+ new mw.Api().saveOption( 'rcdays', newValue );
+ // Update the preference for this session
+ mw.user.options.set( 'rcdays', newValue );
+ }
+ };
+
/**
* Synchronize the URL with the current state of the filters
* without adding an history entry.
this.uriProcessor.updateModelBasedOnQuery( new mw.Uri().query );
+ // Update the sticky preferences, in case we received a value
+ // from the URL
+ this.updateStickyPreferences();
+
// Only update and fetch new results if it is requested
if ( fetchChangesList ) {
this.updateChangesList();
data = defaultSavedQueryItem.getData();
queryHighlights = data.highlights || {};
- savedParams = this.filtersModel.getParametersFromFilters( data.filters || {} );
+ savedParams = this.filtersModel.getParametersFromFilters(
+ // Merge filters with sticky values
+ $.extend( true, {}, data.filters, this.filtersModel.getStickyFiltersState() )
+ );
// Translate highlights to parameters
savedHighlights.highlight = String( Number( queryHighlights.highlight ) );
*/
mw.rcfilters.Controller.prototype._fetchChangesList = function () {
var uri = this._getUpdatedUri(),
+ stickyParams = this.filtersModel.getStickyParams(),
requestId = ++this.requestCounter,
latestRequest = function () {
return requestId === this.requestCounter;
}.bind( this );
+ // Sticky parameters override the URL params
+ // this is to make sure that whether we represent
+ // the sticky params in the URL or not (they may
+ // be normalized out) the sticky parameters are
+ // always being sent to the server with their
+ // current/default values
+ uri.extend( stickyParams );
+
return $.ajax( uri.toString(), { contentType: 'html' } )
.then(
// Success
* @return {Object} Empty parameter state
*/
mw.rcfilters.UriProcessor.prototype._getEmptyParameterState = function () {
- return this.emptyParameterState;
+ // Override empty parameter state with the sticky parameter values
+ return $.extend( true, {}, this.emptyParameterState, this.filtersModel.getStickyParams() );
};
/**
* @param {string} filterName Chosen filter name
*/
mw.rcfilters.ui.ChangesLimitButtonWidget.prototype.onPopupLimit = function ( filterName ) {
+ var item = this.limitGroupModel.getItemByName( filterName );
+
this.controller.toggleFilterSelect( filterName, true );
+ this.controller.updateLimitDefault( item.getParamName() );
this.button.popup.toggle( false );
};
* @param {string} filterName Chosen filter name
*/
mw.rcfilters.ui.DateButtonWidget.prototype.onPopupDays = function ( filterName ) {
+ var item = this.daysGroupModel.getItemByName( filterName );
+
this.controller.toggleFilterSelect( filterName, true );
+ this.controller.updateDaysDefault( item.getParamName() );
this.button.popup.toggle( false );
};
}, {
name: 'group6',
type: 'boolean',
+ isSticky: true,
filters: [
- { name: 'group6option1', label: 'group6option1-label', description: 'group5option1-desc' },
- { name: 'group6option2', label: 'group6option2-label', description: 'group5option2-desc', default: true, useDefaultAsBaseValue: true },
- { name: 'group6option3', label: 'group6option3-label', description: 'group5option3-desc', default: true }
+ { name: 'group6option1', label: 'group6option1-label', description: 'group6option1-desc' },
+ { name: 'group6option2', label: 'group6option2-label', description: 'group6option2-desc', default: true },
+ { name: 'group6option3', label: 'group6option3-label', description: 'group6option3-desc', default: true }
+ ]
+ }, {
+ name: 'group7',
+ type: 'single_option',
+ isSticky: true,
+ default: 'group7option2',
+ filters: [
+ { name: 'group7option1', label: 'group7option1-label', description: 'group7option1-desc' },
+ { name: 'group7option2', label: 'group7option2-label', description: 'group7option2-desc' },
+ { name: 'group7option3', label: 'group7option3-label', description: 'group7option3-desc' }
]
} ],
viewsDefinition = {
group6option1: '0',
group6option2: '1',
group6option3: '1',
+ group7: 'group7option2',
namespace: ''
},
baseParamRepresentation = {
group5: 'option1',
group6option1: '0',
group6option2: '1',
- group6option3: '0',
+ group6option3: '1',
+ group7: 'group7option2',
namespace: ''
},
baseFilterRepresentation = {
group5__option3: false,
group6__group6option1: false,
group6__group6option2: true,
- group6__group6option3: false,
+ group6__group6option3: true,
+ group7__group7option1: false,
+ group7__group7option2: true,
+ group7__group7option3: false,
namespace__0: false,
namespace__1: false,
namespace__2: false,
group5__option3: { selected: false, conflicted: false, included: false },
group6__group6option1: { selected: false, conflicted: false, included: false },
group6__group6option2: { selected: true, conflicted: false, included: false },
- group6__group6option3: { selected: false, conflicted: false, included: false },
+ group6__group6option3: { selected: true, conflicted: false, included: false },
+ group7__group7option1: { selected: false, conflicted: false, included: false },
+ group7__group7option2: { selected: true, conflicted: false, included: false },
+ group7__group7option3: { selected: false, conflicted: false, included: false },
namespace__0: { selected: false, conflicted: false, included: false },
namespace__1: { selected: false, conflicted: false, included: false },
namespace__2: { selected: false, conflicted: false, included: false },
defaultParameters,
'Default parameters are stored properly per filter and group'
);
+
+ // Change sticky filter
+ model.toggleFiltersSelected( {
+ group7__group7option1: true
+ } );
+
+ // Make sure defaults have changed
+ assert.deepEqual(
+ model.getDefaultParams(),
+ $.extend( true, {}, defaultParameters, {
+ group7: 'group7option1'
+ } ),
+ 'Default parameters are stored properly per filter and group'
+ );
} );
QUnit.test( 'Finding matching filters', function ( assert ) {