MaxSem has submitted this change and it was merged. Change subject: Commit extension boilerplate and existing code from github ......................................................................
Commit extension boilerplate and existing code from github https://github.com/jdlrobson/AshCatchem Change-Id: I82d787b4cfbd2c704db7680859c71e0e2d3f4c94 --- A .gitignore A .jsbeautifyrc A .jscsrc A .jscsrctest.js A .jshintignore A .jshintrc A .rubocop.yml A .rubocop_todo.yml A .svgo.yml A Gather.alias.php A Gather.php A Gemfile A Gemfile.lock A Gruntfile.js A README.txt A extension.json A i18n/en.json A i18n/qqq.json A images/icons/next.svg A includes/Gather.hooks.php A includes/Resources.php A includes/models/Collection.php A includes/models/CollectionsList.php A includes/specials/SpecialGather.php A includes/stores/CollectionStore.php A includes/stores/WatchlistCollectionStore.php A includes/views/CollectionItemCardView.php A includes/views/CollectionView.php A includes/views/CollectionsListItemCardView.php A includes/views/CollectionsListView.php A includes/views/UserNotFoundView.php A includes/views/View.php A package.json A resources/ext.collections.styles/collections.less A resources/ext.collections.styles/icons.less A resources/ext.collections.styles/images/icons/next.svg A tests/.htaccess A tests/browser/.gitignore 38 files changed, 1,767 insertions(+), 0 deletions(-) Approvals: MaxSem: Verified; Looks good to me, approved diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..953e79e --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*~ +.*.swp +/composer.phar +node_modules/ +tmp/ +docs/ +composer.phar +.DS_Store +composer.lock +tests/report diff --git a/.jsbeautifyrc b/.jsbeautifyrc new file mode 100644 index 0000000..cef4838 --- /dev/null +++ b/.jsbeautifyrc @@ -0,0 +1,18 @@ +{ + "indent_level": 0, + "indent_with_tabs": true, + "preserve_newlines": true, + "max_preserve_newlines": 10, + "jslint_happy": false, + "space_after_anon_function": true, + "brace_style": "collapse", + "keep_array_indentation": false, + "keep_function_indentation": false, + "space_before_conditional": true, + "space_in_paren": true, + "break_chained_methods": false, + "eval_code": false, + "unescape_strings": false, + "wrap_line_length": 0, + "end_with_newline": true +} diff --git a/.jscsrc b/.jscsrc new file mode 100644 index 0000000..f1c731b --- /dev/null +++ b/.jscsrc @@ -0,0 +1,32 @@ +{ + "preset": "wikimedia", + "requireMultipleVarDecl": "onevar", + "requireLineBreakAfterVariableAssignment": true, + "disallowOperatorBeforeLineBreak": ["."], + "requireBlocksOnNewline": true, + "requireLineFeedAtFileEnd": true, + "requirePaddingNewLinesInObjects": true, + "disallowDanglingUnderscores": null, + "requireSpacesInsideParentheses": "all", + "disallowImplicitTypeConversion": ["numeric", "boolean", "binary", "string"], + "validateJSDoc": { + "checkParamNames": true, + "requireParamTypes": true, + "checkRedundantParams": true + }, + "validateIndentation": "\t", + "excludeFiles": ["javascripts/externals/**/*.js", "javascripts/README.md"], + "additionalRules": [ + "node_modules/jscs-jsdoc/lib/rules/*.js" + ], + "jsDoc": { + "checkParamNames": true, + "checkRedundantParams": true, + "enforceExistence": true, + "checkReturnTypes": true, + "checkRedundantReturns": true, + "requireReturnTypes": true, + "checkTypes": "capitalizedNativeCase", + "checkParamExistence": true + } +} diff --git a/.jscsrctest.js b/.jscsrctest.js new file mode 100644 index 0000000..6435c26 --- /dev/null +++ b/.jscsrctest.js @@ -0,0 +1,6 @@ +var fs = require( 'fs' ), + config = JSON.parse( fs.readFileSync( '.jscsrc' ) ); + +delete config.jsDoc; + +module.exports = exports = config; diff --git a/.jshintignore b/.jshintignore new file mode 100644 index 0000000..57e0837 --- /dev/null +++ b/.jshintignore @@ -0,0 +1,5 @@ +javascripts/externals +javascripts/README.md +tests/js/fixtures.js +tests/externals +styleguide-template diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..df30cb2 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,21 @@ +{ + "globals": { + "console": true, + "jQuery": true, + "JsDiff": true, + "Hogan": true, + "QUnit": true, + "mw": true, + "OO": true + }, + + "browser": true, + "curly": true, + "eqeqeq": true, + "forin": false, + "onevar": true, + "trailing": true, + "undef" : true, + "unused": true, + "supernew": true +} diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..cc32da4 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1 @@ +inherit_from: .rubocop_todo.yml diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml new file mode 100644 index 0000000..78f2eba --- /dev/null +++ b/.rubocop_todo.yml @@ -0,0 +1,39 @@ +# This configuration was generated by `rubocop --auto-gen-config` +# on 2014-12-05 09:03:35 -0700 using RuboCop version 0.27.1. +# The point is for the user to remove these configuration records +# one by one as the offenses are removed from the code base. +# Note that changes in the inspected code, or installation of new +# versions of RuboCop, may require this file to be generated again. + +# Offense count: 9 +Lint/AmbiguousRegexpLiteral: + Enabled: false + +# Offense count: 1 +Lint/ParenthesesAsGroupedExpression: + Enabled: false + +# Offense count: 1 +# Configuration parameters: CountComments. +Metrics/ClassLength: + Max: 109 + +# Offense count: 54 +# Configuration parameters: AllowURI, URISchemes. +Metrics/LineLength: + Max: 428 + +# Offense count: 13 +Style/Documentation: + Enabled: false + +# Offense count: 1 +# Configuration parameters: AllowedVariables. +Style/GlobalVars: + Enabled: false + +# Offense count: 1 +# Cop supports --auto-correct. +Style/RedundantSelf: + Enabled: false + diff --git a/.svgo.yml b/.svgo.yml new file mode 100644 index 0000000..52ae3ca --- /dev/null +++ b/.svgo.yml @@ -0,0 +1,5 @@ +plugins: + - removeXMLProcInst: false + - cleanupIDs: false + - collapseGroups: false + - mergePaths: false \ No newline at end of file diff --git a/Gather.alias.php b/Gather.alias.php new file mode 100644 index 0000000..8d38483 --- /dev/null +++ b/Gather.alias.php @@ -0,0 +1,15 @@ +<?php +/** + * Aliases for Gather extension + * + * @file + * @ingroup Extensions + */ +// @codingStandardsIgnoreFile + +$specialPageAliases = array(); + +/** English (English) */ +$specialPageAliases['en'] = array( + 'Gather' => array( 'Gather' ), +); diff --git a/Gather.php b/Gather.php new file mode 100644 index 0000000..19146fd --- /dev/null +++ b/Gather.php @@ -0,0 +1,71 @@ +<?php +/** + * Extension Gather + * + * @file + * @ingroup Extensions + * @author Jon Robson + * @author Joaquin Hernandez + * @author Rob Moen + * @licence GNU General Public Licence 2.0 or later + */ + +// Needs to be called within MediaWiki; not standalone +if ( !defined( 'MEDIAWIKI' ) ) { + echo "This is a MediaWiki extension and cannot run standalone.\n"; + die( -1 ); +} + +// Extension credits that will show up on Special:Version +$wgExtensionCredits['other'][] = array( + 'path' => __FILE__, + 'name' => 'Gather', + 'author' => array( 'Jon Robson', 'Joaquin Hernandez', 'Rob Moen' ), + 'descriptionmsg' => 'gather-desc', + 'url' => 'https://www.mediawiki.org/wiki/Gather', + 'license-name' => 'GPL-2.0+', +); + +$wgMessagesDirs['Gather'] = __DIR__ . '/i18n'; +$wgExtensionMessagesFiles['GatherAlias'] = __DIR__ . "/Gather.alias.php"; + +function efGatherExtensionSetup() { + // FIXME: This doesn't do anything as if mobilefrontend is not present + // The reported error is "This requires Gather." + if ( !defined( 'MOBILEFRONTEND' ) ) { + echo "Gather extension requires MobileFrontend.\n"; + die( -1 ); + } +} + +// autoload extension classes +$autoloadClasses = array ( + 'Gather\Hooks' => 'Gather.hooks', + + 'Gather\Collection' => 'models/Collection', + 'Gather\CollectionsList' => 'models/CollectionsList', + + 'Gather\CollectionStore' => 'stores/CollectionStore', + 'Gather\WatchlistCollectionStore' => 'stores/WatchlistCollectionStore', + + 'Gather\View' => 'views/View', + 'Gather\UserNotFoundView' => 'views/UserNotFoundView', + 'Gather\CollectionView' => 'views/CollectionView', + 'Gather\CollectionItemCardView' => 'views/CollectionItemCardView', + 'Gather\CollectionsListView' => 'views/CollectionsListView', + 'Gather\CollectionsListItemCardView' => 'views/CollectionsListItemCardView', + + 'Gather\SpecialGather' => 'specials/SpecialGather', +); + +foreach ( $autoloadClasses as $className => $classFilename ) { + $wgAutoloadClasses[$className] = __DIR__ . "/includes/$classFilename.php"; +} + +$wgExtensionFunctions[] = 'efGatherExtensionSetup'; + +$wgSpecialPages['Gather'] = 'Gather\SpecialGather'; + +// ResourceLoader modules +require_once __DIR__ . "/includes/Resources.php"; + diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..9122bd5 --- /dev/null +++ b/Gemfile @@ -0,0 +1,9 @@ +# ruby=ruby-2.1.1 +# ruby-gemset=Gather + +source 'https://rubygems.org' + +gem 'chunky_png' +gem 'jsduck' +gem 'mediawiki_selenium', '~> 0.3.2' +gem 'rubocop', require: false diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..a382242 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,111 @@ +GEM + remote: https://rubygems.org/ + specs: + ast (2.0.0) + astrolabe (1.3.0) + parser (>= 2.2.0.pre.3, < 3.0) + builder (3.2.2) + childprocess (0.5.5) + ffi (~> 1.0, >= 1.0.11) + chunky_png (1.3.3) + cucumber (1.3.17) + builder (>= 2.1.2) + diff-lcs (>= 1.1.3) + gherkin (~> 2.12) + multi_json (>= 1.7.5, < 2.0) + multi_test (>= 0.1.1) + data_magic (0.20) + faker (>= 1.1.2) + yml_reader (>= 0.4) + diff-lcs (1.2.5) + dimensions (1.2.0) + domain_name (0.5.22) + unf (>= 0.0.5, < 1.0.0) + faker (1.4.3) + i18n (~> 0.5) + faraday (0.9.0) + multipart-post (>= 1.2, < 3) + faraday-cookie_jar (0.0.6) + faraday (>= 0.7.4) + http-cookie (~> 1.0.0) + ffi (1.9.6) + gherkin (2.12.2) + multi_json (~> 1.3) + headless (1.0.2) + http-cookie (1.0.2) + domain_name (~> 0.5) + i18n (0.6.11) + jsduck (5.3.4) + dimensions (~> 1.2.0) + json (~> 1.8.0) + parallel (~> 0.7.1) + rdiscount (~> 2.1.6) + rkelly-remix (~> 0.0.4) + json (1.8.1) + mediawiki_api (0.3.0) + faraday (~> 0.9, >= 0.9.0) + faraday-cookie_jar (~> 0.0, >= 0.0.6) + mediawiki_selenium (0.3.2) + cucumber (~> 1.3, >= 1.3.10) + headless (~> 1.0, >= 1.0.1) + json (~> 1.8, >= 1.8.1) + mediawiki_api (~> 0.2, >= 0.2.1) + page-object (~> 1.0) + rest-client (~> 1.6, >= 1.6.7) + rspec-expectations (~> 2.14, >= 2.14.4) + syntax (~> 1.2, >= 1.2.0) + mime-types (2.4.3) + multi_json (1.10.1) + multi_test (0.1.1) + multipart-post (2.0.0) + netrc (0.9.0) + page-object (1.0.2) + page_navigation (>= 0.9) + selenium-webdriver (>= 2.42.0) + watir-webdriver (>= 0.6.9) + page_navigation (0.9) + data_magic (>= 0.14) + parallel (0.7.1) + parser (2.2.0.pre.8) + ast (>= 1.1, < 3.0) + slop (~> 3.4, >= 3.4.5) + powerpack (0.0.9) + rainbow (2.0.0) + rdiscount (2.1.7.1) + rest-client (1.7.2) + mime-types (>= 1.16, < 3.0) + netrc (~> 0.7) + rkelly-remix (0.0.6) + rspec-expectations (2.99.2) + diff-lcs (>= 1.1.3, < 2.0) + rubocop (0.27.1) + astrolabe (~> 1.3) + parser (>= 2.2.0.pre.7, < 3.0) + powerpack (~> 0.0.6) + rainbow (>= 1.99.1, < 3.0) + ruby-progressbar (~> 1.4) + ruby-progressbar (1.7.0) + rubyzip (1.1.6) + selenium-webdriver (2.44.0) + childprocess (~> 0.5) + multi_json (~> 1.0) + rubyzip (~> 1.0) + websocket (~> 1.0) + slop (3.6.0) + syntax (1.2.0) + unf (0.1.4) + unf_ext + unf_ext (0.0.6) + watir-webdriver (0.6.11) + selenium-webdriver (>= 2.18.0) + websocket (1.2.1) + yml_reader (0.4) + +PLATFORMS + ruby + +DEPENDENCIES + chunky_png + jsduck + mediawiki_selenium (~> 0.3.2) + rubocop diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000..dfb0395 --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,160 @@ +/*! + * Grunt file + * + * @package Gather + */ + +/*jshint node:true, strict:false*/ +module.exports = function ( grunt ) { + var MW_INSTALL_PATH = grunt.option( 'MW_INSTALL_PATH' ) || process.env.MW_INSTALL_PATH; + + grunt.loadNpmTasks( 'grunt-contrib-jshint' ); + grunt.loadNpmTasks( 'grunt-jscs' ); + grunt.loadNpmTasks( 'grunt-qunit-istanbul' ); + grunt.loadNpmTasks( 'grunt-contrib-watch' ); + grunt.loadNpmTasks( 'grunt-notify' ); + grunt.loadNpmTasks( 'grunt-svg2png' ); + grunt.loadNpmTasks( 'grunt-jsduck' ); + grunt.loadNpmTasks( 'grunt-contrib-clean' ); + grunt.loadNpmTasks( 'grunt-mkdir' ); + + grunt.initConfig( { + URL: process.env.MEDIAWIKI_URL || 'http://127.0.0.1:8080/w/index.php/', + QUNIT_DEBUG: ( process.env.QUNIT_DEBUG && '&debug=true' || '' ), + QUNIT_FILTER: ( process.env.QUNIT_FILTER && '&filter=' + process.env.QUNIT_FILTER ) || '', + QUNIT_MODULE: ( process.env.QUNIT_MODULE && '&module=' + process.env.QUNIT_MODULE ) || '', + files: { + js: 'resources/**/*.js', + jsTests: 'tests/qunit/**/*.js' + }, + jshint: { + options: { + jshintrc: true + }, + tests: '<%= files.jsTests %>', + sources: [ + '<%= files.js %>', + '!<%= files.jsExternals %>' + ] + }, + jscs: { + main: [ + '<%= files.js %>' + ], + test: { + options: { + config: '.jscsrctest.js', + }, + files: { + src: '<%= files.jsTests %>' + } + } + }, + qunit: { + all: { + options: { + timeout: 20000, + urls: [ + '<%= URL %>Special:JavaScriptTest/qunit?useformat=mobile' + + '<%= QUNIT_DEBUG %><%= QUNIT_FILTER %><%= QUNIT_MODULE %>' + ] + } + }, + cov: { + options: { + timeout: 20000, + urls: [ + '<%= URL %>Special:JavaScriptTest/qunit?debug=true&useformat=mobile' + + '<%= QUNIT_FILTER %><%= QUNIT_MODULE %>' + ], + coverage: { + prefixUrl: 'w/', // Prefix url on the server + baseUrl: '../../', // Path to assets from the server (extensions/Mobile...) + src: [ '<%= files.js %>', '!<%= files.jsExternals %>' ], + instrumentedFiles: 'tests/report/tmp', + htmlReport: 'tests/report' + } + } + } + }, + watch: { + lint: { + files: [ '<%= files.js %>', '<%= files.jsTests %>' ], + tasks: [ 'lint' ] + }, + scripts: { + files: [ '<%= files.js %>', '<%= files.jsTests %>' ], + tasks: [ 'test' ] + }, + configFiles: { + files: [ 'Gruntfile.js' ], + options: { + reload: true + } + } + }, + mkdir: { + jsdocs: { + options: { + create: [ 'docs/js' ] + } + } + }, + clean: { + jsdocs: [ 'docs/js' ] + }, + jsduck: { + main: { + src: [ '<%= files.js %>', '!<%= files.jsExternals %>' ], + dest: 'docs/js', + options: { + 'builtin-classes': true, + 'external': [ + 'Hogan.Template', + 'HandleBars.Template', + 'jQuery.Deferred', + 'jQuery.Event', + 'jQuery.Object', + 'jqXHR', + 'File', + 'mw.user', + 'OO.EventEmitter' + ], + 'ignore-global': true, + 'tags': './.docs/jsduckCustomTags.rb', + 'warnings': [ '-nodoc(class,public)', '-dup_member', '-link_ambiguous' ] + } + } + } + } ); + + grunt.registerTask( 'checkInstallPath', 'Check if the install path is set', function () { + checkInstallPathNotFound( MW_INSTALL_PATH, grunt ); + } ); + + grunt.registerTask( 'lint', [ 'jshint', 'jscs' ] ); + grunt.registerTask( 'docs', [ 'checkInstallPath', 'clean:jsdocs', 'mkdir:jsdocs', 'jsduck:main' ] ); + + // grunt test will be run by npm test which will be run by Jenkins + // Do not execute qunit here, or other tasks that require full mediawiki + // running. + grunt.registerTask( 'test', [ 'lint' ] ); + + grunt.registerTask( 'default', [ 'test' ] ); +}; + +/** + * Checks if the path is set, and if not it prints an error message and bails + * out + */ +function checkInstallPathNotFound( MW_INSTALL_PATH, grunt ) { + if ( !MW_INSTALL_PATH ) { + grunt.log.error( + 'MW_INSTALL_PATH is not set. Please set it to your root mediawiki installation or pass the --MW_INSTALL_PATH to grunt.\n\n' + + '\n export MW_INSTALL_PATH=/Users/johndoe/dev/mediawiki' + + '\n MW_INSTALL_PATH=../../ grunt' + + '\n grunt --MW_INSTALL_PATH=../../' + ); + process.exit( 1 ); + } +} diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..d881dfc --- /dev/null +++ b/README.txt @@ -0,0 +1,2 @@ +This is a skunkworks project with the goal of iterating quickly. +When we have something ready to put in front of users it will be productionised. diff --git a/extension.json b/extension.json new file mode 100644 index 0000000..c15a00d --- /dev/null +++ b/extension.json @@ -0,0 +1,95 @@ +{ + "name": "Gather", + "author": [ + "Jon Robson", + "Joaquin Hernandez", + "Rob Moen" + ], + "url": "https://www.mediawiki.org/wiki/Gather", + "descriptionmsg": "gather-desc", + "license-name": "GPL-2.0+", + "type": "other", + "ExtensionFunctions": [ + "efGatherExtensionSetup" + ], + "SpecialPages": { + "Gather": "Gather\\SpecialGather" + }, + "MessagesDirs": { + "Gather": [ + "i18n" + ] + }, + "ExtensionMessagesFiles": { + "GatherAlias": "Gather.alias.php" + }, + "AutoloadClasses": { + "Gather\\Hooks": "includes/Gather.hooks.php", + "Gather\\Collection": "includes/models/Collection.php", + "Gather\\CollectionsList": "includes/models/CollectionsList.php", + "Gather\\CollectionStore": "includes/stores/CollectionStore.php", + "Gather\\WatchlistCollectionStore": "includes/stores/WatchlistCollectionStore.php", + "Gather\\View": "includes/views/View.php", + "Gather\\UserNotFoundView": "includes/views/UserNotFoundView.php", + "Gather\\CollectionView": "includes/views/CollectionView.php", + "Gather\\CollectionItemCardView": "includes/views/CollectionItemCardView.php", + "Gather\\CollectionsListView": "includes/views/CollectionsListView.php", + "Gather\\CollectionsListItemCardView": "includes/views/CollectionsListItemCardView.php", + "Gather\\SpecialGather": "includes/specials/SpecialGather.php" + }, + "ResourceModules": { + "ext.collections.icons": { + "localBasePath": "includes", + "remoteExtPath": "Gather", + "targets": [ + "mobile", + "desktop" + ], + "class": "ResourceLoaderImageModule", + "prefix": "mw-ui", + "images": { + "icon": { + "collections-read-more:before": "images/icons/next.svg" + } + } + }, + "ext.collections.styles": { + "localBasePath": "includes", + "remoteExtPath": "Gather", + "targets": [ + "mobile", + "desktop" + ], + "styles": [ + "resources/ext.collections.styles/icons.less", + "resources/ext.collections.styles/collections.less" + ], + "dependencies": [ + "mediawiki.ui.anchor", + "skins.minerva.special.styles" + ], + "position": "top", + "group": "other" + } + }, + "config": { + "GatherResourceBoilerplate": { + "localBasePath": "extensions/Gather/includes", + "remoteExtPath": "Gather" + }, + "GatherResourceFileModuleBoilerplate": { + "localBasePath": "extensions/Gather/includes", + "remoteExtPath": "Gather", + "targets": [ + "mobile", + "desktop" + ] + }, + "GatherMobileSpecialPageResourceBoilerplate": { + "localBasePath": "extensions/Gather/includes", + "remoteExtPath": "Gather", + "targets": "mobile", + "group": "other" + } + } +} diff --git a/i18n/en.json b/i18n/en.json new file mode 100644 index 0000000..58d5a73 --- /dev/null +++ b/i18n/en.json @@ -0,0 +1,15 @@ +{ + "@metadata": { + "authors": [] + }, + "gather-desc": "Component of Mobile Frontend allowing users to curate lists.", + "gather-anon-view-lists": "You need to be logged in to see your Collections.", + "gather-watchlist-title": "Watchlist", + "gather-watchlist-description": "A list of pages that I am interested in.", + "gather-lists-title": "Collections", + "gather-read-more": "Read more", + "gather-private": "Private", + "gather-article-count": "$1 {{PLURAL:$1|article|articles}}", + "gather-empty": "Nothing in this collection yet...", + "gather-empty-footer": "I don't know how you got here but this is a sad place." +} diff --git a/i18n/qqq.json b/i18n/qqq.json new file mode 100644 index 0000000..8ae0ecf --- /dev/null +++ b/i18n/qqq.json @@ -0,0 +1,15 @@ +{ + "@metadata": { + "authors": [] + }, + "gather-desc": "{{desc|name=Gather|url=https://www.mediawiki.org/wiki/Extension:Gather}}", + "gather-anon-view-lists": "Text shown when trying to see your own collections on [[Special:Gather]] but the user is not logged in.", + "gather-watchlist-title": "Title used for special casing the Watchlist collection on the [[Special:Gather]] page. ", + "gather-watchlist-description": "Default description for special casing the Watchlist collection on the [[Special:Gather]] page.", + "gather-lists-title": "Title for [[Special:Gather]] when displaying user curated lists.", + "gather-read-more": "Label for the read more link used on [[Special:Gather]].", + "gather-private": "Label for a collection when it is not publicly visible", + "gather-article-count": "Expression of the number of articles in a collection.\nParameter:\n * $1 - number of articles in the collection", + "gather-empty": "Message shown on an empty rendered collection on [[Special:Gather]].", + "gather-empty-footer": "Footnote shown on an empty rendered collection on [[Special:Gather]]." +} diff --git a/images/icons/next.svg b/images/icons/next.svg new file mode 100644 index 0000000..5b2290e --- /dev/null +++ b/images/icons/next.svg @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 17.5 26" enable-background="new 0 0 17.5 26"> + <g> + <g> + <path fill="#347BFF" d="M4.6 24.3l-3.1-3 8.4-8.4-8.4-8.4 3.1-3L16 12.9z"/> + </g> + </g> +</svg> diff --git a/includes/Gather.hooks.php b/includes/Gather.hooks.php new file mode 100644 index 0000000..0fa36bb --- /dev/null +++ b/includes/Gather.hooks.php @@ -0,0 +1,18 @@ +<?php +/** + * Gather.hooks.php + */ + +namespace Gather; + +/** + * Hook handlers for Gather extension + * + * Hook handler method names should be in the form of: + * on<HookName>() + * For intance, the hook handler for the 'RequestContextCreateSkin' would be called: + * onRequestContextCreateSkin() + */ +class Hooks { + +} diff --git a/includes/Resources.php b/includes/Resources.php new file mode 100644 index 0000000..3509268 --- /dev/null +++ b/includes/Resources.php @@ -0,0 +1,78 @@ +<?php +/** + * Definition of Gather's ResourceLoader modules. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +if ( !defined( 'MEDIAWIKI' ) ) { + die( 'Not an entry point.' ); +} + +/** + * A boilerplate for RL modules that do not support templates + * Agnostic to whether desktop or mobile specific. + */ +$wgGatherResourceBoilerplate = array( + 'localBasePath' => __DIR__, + 'remoteExtPath' => 'Gather', +); + +/** + * A mobile enabled ResourceLoaderFileModule template + */ +$wgGatherResourceFileModuleBoilerplate = $wgGatherResourceBoilerplate + array( + 'targets' => array( 'mobile', 'desktop' ), +); + +/** + * A boilerplate containing common properties for all RL modules served to mobile site special pages + * Restricted to mobile site. + */ +$wgGatherMobileSpecialPageResourceBoilerplate = $wgGatherResourceBoilerplate + array( + 'targets' => 'mobile', + 'group' => 'other', +); + +$wgResourceModules = array_merge( $wgResourceModules, array( + + 'ext.collections.icons' => $wgGatherResourceFileModuleBoilerplate + array( + 'class' => 'ResourceLoaderImageModule', + 'prefix' => 'mw-ui', + 'images' => array( + // FIXME: ':before' suffix should be configurable in image module. + 'icon' => array( + 'collections-read-more:before' => 'images/icons/next.svg', + ), + ), + ), + + 'ext.collections.styles' => $wgGatherResourceFileModuleBoilerplate + array( + 'styles' => array( + 'resources/ext.collections.styles/icons.less', + 'resources/ext.collections.styles/collections.less', + ), + 'dependencies' => array( + 'mediawiki.ui.anchor', + 'skins.minerva.special.styles' + ), + 'position' => 'top', + 'group' => 'other', + ), + +) ); diff --git a/includes/models/Collection.php b/includes/models/Collection.php new file mode 100644 index 0000000..8006468 --- /dev/null +++ b/includes/models/Collection.php @@ -0,0 +1,170 @@ +<?php + +/** + * Collection.php + */ + +namespace Gather; + +/** + * A collection of pages, which are represented by the MobilePage class. + */ +class Collection implements IteratorAggregate { + + /** + * The internal collection of pages. + * + * @var MobilePage[] + */ + protected $pages = array(); + + /** + * Owner of collection + * @var User + */ + protected $owner; + + /** + * @var string + */ + protected $title; + + /** + * @var string + */ + protected $description; + + /** + * Whether collection is public or private + * Collection by default is true + * + * @var bool + */ + protected $public; + + /** + * @param User $user User that owns the collection + * @param string $title Title of the collection + * @param string $description Description of the collection + */ + public function __construct( User $user, $title = '', $description = '', $public = true ) { + $this->user = $user; + $this->title = $title; + $this->description = $description; + $this->public = $public; + } + + /** + * The internal id of a collection + * + * @var int id + */ + protected $id; + + /** + * Adds a page to the collection. + * + * @param MobilePage $page + */ + public function add( MobilePage $page ) { + $this->pages[] = $page; + } + + /** + * Gets the iterator for the internal array + * + * @return ArrayIterator + */ + public function getIterator() { + return new ArrayIterator( $this->pages ); + } + + /** + * @return User + */ + public function getOwner() { + return $this->owner; + } + + /** + * @return string + */ + public function getTitle() { + return $this->title; + } + + /** + * @return string + */ + public function getDescription() { + return $this->description; + } + + /** + * Returns if the list is public + * + * @return boolean + */ + public function isPublic() { + return $this->public; + } + + /** + * Set if the list is public + * + * @param boolean $public + */ + public function setPublic( $public ) { + $this->public = $public; + } + + /** + * @return int id The internal id of a collection + */ + public function getId() { + return $this->id; + } + + /** + * Returns pages count + * + * @return int count of pages in collection + */ + public function getCount() { + return count( $this->pages ); + } + + /** + * Return local url for collection + * Example: /wiki/Special:Gather/user/id + * + * @return string localized url for collection + */ + public function getUrl() { + return SpecialPage::getTitleFor( 'Gather' ) + ->getSubpage( $this->getOwner() ) + ->getSubpage( $this->getId() ) + ->getLocalURL(); + } + + /** + * @return array list of pages + */ + public function getPages() { + return $this->pages; + } + + /** + * Adds an array of titles to the collection + * + * @param CollectionStore $store + */ + public function load( CollectionStore $store ) { + $this->id = $store->getId(); + $titles = $store->getTitles(); + foreach ( $titles as $title ) { + $this->add( new MobilePage( $title ) ); + } + } + +} diff --git a/includes/models/CollectionsList.php b/includes/models/CollectionsList.php new file mode 100644 index 0000000..4e20490 --- /dev/null +++ b/includes/models/CollectionsList.php @@ -0,0 +1,71 @@ +<?php + +/** + * CollectionsList.php + */ + +namespace Gather; + +/** + * A list of collections, which are represented by the Collection class. + */ +class CollectionsList implements IteratorAggregate { + /** + * @var Gather\Collection[] Internal list of collections. + */ + protected $lists = array(); + + /** + * @var bool if the list can show private collections or not + */ + protected $includePrivate; + + /** + * Creates a list of collection cards + * + * @param User $user collection list owner + * @param boolean $includePrivate if the list can show private collections or not + */ + public function __construct( User $user, $includePrivate = false ) { + $this->includePrivate = $includePrivate; + + // Get watchlist collection (private) + // Directly avoid adding if not owner + if ( $includePrivate ) { + $watchlist = new Gather\Collection( + $user, + wfMessage( 'gather-watchlist-title' ), + wfMessage( 'gather-watchlist-description' ), + false + ); + $watchlist->load( new WatchlistCollectionStore( $user ) ); + + $this->add( $watchlist ); + } + + // FIXME: Add from UserCollectionStore + } + + /** + * Adds a page to the collection. + * If the collection to add is private, and this collection list does not include + * private items, the collection won't be added + * + * @param Gather\Collection $collection + */ + public function add( Gather\Collection $collection ) { + if ( $this->includePrivate || + ( !$this->includePrivate && $collection->isPublic() ) ) { + $this->lists[] = $collection; + } + } + + /** + * Gets the iterator for the internal array + * + * @return ArrayIterator + */ + public function getIterator() { + return new ArrayIterator( $this->lists ); + } +} diff --git a/includes/specials/SpecialGather.php b/includes/specials/SpecialGather.php new file mode 100644 index 0000000..3af1499 --- /dev/null +++ b/includes/specials/SpecialGather.php @@ -0,0 +1,120 @@ +<?php +/** + * SpecialGather.php + */ + +namespace Gather; + +/** + * Render a collection of articles. + */ +class SpecialGather extends SpecialPage { + + public function __construct() { + parent::__construct( 'Gather' ); + } + + /** + * Render the special page and redirect the user to the editor (if page exists) + * + * @param string $subpage The name of the page to edit + */ + public function execute( $subpage ) { + + if ( $subpage ) { + $args = explode( '/', $subpage ); + // If there is a user argument, that's what we want to use + if ( isset( $args[0] ) ) { + // Show specified user's collections + $user = User::newFromName( $args[0] ); + } else { + // Otherwise use current user + $user = $this->getUser(); + } + } else { + // For listing own lists, you need to be logged in + $this->requireLogin( 'gather-anon-view-lists' ); + $user = $this->getUser(); + } + + if ( !( $user && $user->getId() ) ) { + // Invalid user + $this->renderUserNotFoundError(); + } else { + if ( isset( $args ) && isset( $args[1] ) ) { + $id = $args[1]; + $this->renderUserCollection( $user, $id ); + } else { + $this->renderUserCollectionsList( $user ); + } + } + } + + /** + * Render an error when the user was not found + */ + public function renderUserNotFoundError() { + $this->render( new Gather\UserNotFoundView() ); + } + + /** + * Renders a user collection + * + * @param User $user collection owner + * @param int $id collection id + */ + public function renderUserCollection( User $user, $id ) { + $collection = new Gather\Collection( + $user, + $this->msg( 'gather-watchlist-title' ), + $this->msg( 'gather-watchlist-description' ) + ); + // Watchlist lives at id 0 + if ( (int)$id === 0 ) { + // Watchlist is private + $collection->setPublic( false ); + if ( $this->isOwner( $user ) ) { + $collection->load( new WatchlistCollectionStore( $user ) ); + } + } + // FIXME: For empty-collection and not-allowed-to-see-this we are doing the + // same thing right now. + $this->render( new Gather\CollectionView( $collection ) ); + } + + /** + * Renders a list of user collections + * + * @param User $user owner of collections + */ + public function renderUserCollectionsList( User $user ) { + $collectionsList = new Gather\CollectionsList( $user, $this->isOwner( $user ) ); + $this->render( new Gather\CollectionsListView( $collectionsList ) ); + } + + /** + * Render the special page using CollectionView and given collection + * + * @param View $view + */ + public function render( $view ) { + $out = $this->getOutput(); + $this->setHeaders(); + $out->setProperty( 'unstyledContent', true ); + $out->addModules( array( 'ext.collections.styles' ) ); + $out->setPageTitle( $view->getTitle() ); + $view->render( $out ); + } + + /** + * Returns if the user viewing the page is the owner of the collection/list + * we are viewing + * + * @param User $user user owner of the current page + * + * @return boolean + */ + private function isOwner( User $user ) { + return $this->getUser()->getName() == $user->getName(); + } +} diff --git a/includes/stores/CollectionStore.php b/includes/stores/CollectionStore.php new file mode 100644 index 0000000..0f3b0d3 --- /dev/null +++ b/includes/stores/CollectionStore.php @@ -0,0 +1,22 @@ +<?php + +namespace Gather; + +/** + * Abstraction for collection storage. + */ +interface CollectionStore { + /** + * Get titles of all pages in the current collection. + * + * @return array titles + */ + public function getTitles(); + + /** + * Get current collection identifier + * + * @return int id + */ + public function getId(); +} diff --git a/includes/stores/WatchlistCollectionStore.php b/includes/stores/WatchlistCollectionStore.php new file mode 100644 index 0000000..0936a0d --- /dev/null +++ b/includes/stores/WatchlistCollectionStore.php @@ -0,0 +1,61 @@ +<?php + +/** + * Abstraction for watchlist storage. + * FIXME: This should live in core and power Special:EditWatchlist + */ +class WatchlistCollectionStore implements CollectionStore { + /** + * @var title[] + */ + protected $titles = array(); + + /** + * @inheritdoc + */ + public function getTitles() { + return $this->titles; + } + + /** + * @inheritdoc + */ + public function getId() { + // Watchlist has hardcoded id of 0 + return 0; + } + + /** + * Initialise WatchlistCollectionStore from database + * + * @param User $user to lookup watchlist members for + */ + public function __construct( User $user ) { + $list = array(); + $dbr = wfGetDB( DB_SLAVE ); + + $res = $dbr->select( + 'watchlist', + array( 'wl_namespace', 'wl_title'), + array( + 'wl_user' => $user->getId(), + 'wl_namespace' => 0, + ), + __METHOD__, + array( 'LIMIT' => 50,) + ); + + $titles = array(); + if ( $res->numRows() > 0 ) { + foreach ( $res as $row ) { + $title = Title::makeTitle( $row->wl_namespace, $row->wl_title ); + $titles[] = $title; + } + $res->free(); + } + GenderCache::singleton()->doTitlesArray( $titles ); + + $this->titles = $titles; + } + +} diff --git a/includes/views/CollectionItemCardView.php b/includes/views/CollectionItemCardView.php new file mode 100644 index 0000000..a1c3100 --- /dev/null +++ b/includes/views/CollectionItemCardView.php @@ -0,0 +1,60 @@ +<?php +/** + * CollectionItemCardView.php + */ + +namespace Gather; + +/** + * View for an item card in a mobile collection. + */ +class CollectionItemCardView extends Gather\View { + protected $item; + + /** + * Constructor + * @param MobilePage $item + */ + public function __construct( MobilePage $item ) { + $this->item = $item; + } + + /** + * Returns title of collection page + * @returns string collection page title + */ + public function getTitle() { + return $this->item->getTitle()->getText(); + } + + /** + * @inheritdoc + */ + protected function getHtml() { + $page = $this->item; + $title = $page->getTitle(); + $html = Html::openElement( 'div', array( 'class' => 'collection-item' ) ) . + Html::openElement( 'h2', array( 'class' => 'collection-item-title' ) ) . + Html::element( 'a', array( 'href' => $title->getLocalUrl() ), + $this->getTitle() + ). + Html::closeElement( 'h2' ) . + Html::openElement( 'div', array( 'class' => 'collection-item-footer' ) ) . + Html::openElement( 'a', + array( + 'href' => $title->getLocalUrl(), + 'class' => MobileUI::anchorClass( 'progressive' ) + ) + ) . + wfMessage( 'gather-read-more' )->text() . + Html::element( + 'span', + array( 'class' => MobileUI::iconClass( 'collections-read-more', 'element', 'collections-read-more-arrow' ) ), + '' + ) . + Html::closeElement( 'a' ) . + Html::closeElement( 'div' ) . + Html::closeElement( 'div' ); + return $html; + } +} diff --git a/includes/views/CollectionView.php b/includes/views/CollectionView.php new file mode 100644 index 0000000..509cdcb --- /dev/null +++ b/includes/views/CollectionView.php @@ -0,0 +1,122 @@ +<?php +/** + * CollectionView.php + */ + +namespace Gather; + +/** + * Render a mobile card. + */ +class CollectionView extends Gather\View { + /** + * @var Gather\Collection + */ + protected $collection; + + /** + * @param Gather\Collection $collection + */ + public function __construct( Gather\Collection $collection ) { + $this->collection = $collection; + } + + /** + * Returns the rendered html for the collection header + * @param Gather\Collection $collection + * + * @return string Html + */ + private function getHeaderHtml( Gather\Collection $collection ) { + $collection = $this->collection; + $description = $collection->getDescription(); + $owner = $collection->getOwner(); + + $html = Html::openElement( 'div', array( 'class' => 'collection-header' ) ) . + Html::element( 'div', array( 'class' => 'collection-description' ), $description ) . + $this->getOwnerHtml( $owner ) . + Html::closeElement( 'div' ); + + return $html; + } + + /** + * Returns the html for showing the owner on the collection header + * + * @param User $owner Owner of the collection + * @return string Html + */ + private function getOwnerHtml( $owner ) { + $name = $owner->getName(); + $attrs = array( + 'class' => 'collection-owner mw-ui-icon mw-ui-icon-before mw-ui-icon-user', + 'href' => SpecialPage::getTitleFor( 'UserProfile', $name )->getLocalUrl(), + ); + return Html::element( 'span', $attrs, $name ); + } + + /** + * Returns the html for an empty collection + * + * @return string HTML + */ + private function getEmptyCollectionMessage() { + // FIXME: i18n this messagesinclude 'CollectionView.php'; + return Html::openElement( 'div', array( 'class' => 'collection-empty' ) ) . + Html::element( 'h3', array(), wfMessage( 'gather-empty' ) ) . + Html::element( 'div', array(), + wfMessage( 'gather-empty-footer' ) ) . + Html::closeElement( 'div' ); + } + + /** + * Return title of collection + * + * @return string collection title + */ + public function getTitle() { + return $this->collection->getTitle(); + } + + /** + * Returns the html for the items of a collection + * + * @param Gather\Collection + * + * @return string HTML + */ + public function getCollectionItems( Gather\Collection $collection ) { + $html = Html::openElement( 'div', array( 'class' => 'collection-items' ) ); + foreach ( $collection as $item ) { + if ( $item->getTitle()->getNamespace() === NS_MAIN ) { + $view = new CollectionItemCardView( $item ); + $html .= $view->getHtml(); + } + } + // FIXME: Pagination(??) Note the WatchlistCollectionStore + // limits the size of the collection to 50. + // Pagination may or may not be needed. + $html .= Html::closeElement( 'div' ); + return $html; + } + + /** + * @inheritdoc + */ + protected function getHtml() { + $collection = $this->collection; + + $html = Html::openElement( 'div', array( 'class' => 'collection content' ) ) . + $this->getHeaderHtml( $collection ); + + if ( count( $collection->getPages() ) > 0 ) { + $html .= $this->getCollectionItems( $collection ); + } else { + $html .= $this->getEmptyCollectionMessage(); + } + + $html .= Html::closeElement( 'div' ); + + return $html; + } +} diff --git a/includes/views/CollectionsListItemCardView.php b/includes/views/CollectionsListItemCardView.php new file mode 100644 index 0000000..9649cfa --- /dev/null +++ b/includes/views/CollectionsListItemCardView.php @@ -0,0 +1,55 @@ +<?php +/** + * CollectionsListItemCardView.php + */ + +namespace Gather; + +/** + * View for an item card in a mobile collection. + */ +class CollectionsListItemCardView extends Gather\View { + + /** + * @param Gather\Collection $collection + */ + public function __construct( Gather\Collection $collection ) { + $this->collection = $collection; + } + + protected $collection; + + /** + * Return title of collection + * + * @returns string collection title + */ + public function getTitle() { + return $this->collection->getTitle(); + } + + /** + * @inheritdoc + */ + public function getHtml() { + $articleCountMsg = wfMessage( + 'gather-article-count', + $this->collection->getCount() + )->text(); + // FIXME: should consider privacy in collection + $followingMsg = wfMessage( 'gather-private' )->text(); + $collectionUrl = $this->collection->getUrl(); + + $html = Html::openElement( 'div', array( 'class' => 'collection-card' ) ) . + Html::openElement( 'div', array( 'class' => 'collection-card-overlay' ) ) . + Html::openElement( 'div', array( 'class' => 'collection-card-title' ) ) . + Html::element( 'a', array( 'href' => $collectionUrl ), $this->getTitle() ) . + Html::closeElement( 'div' ) . + Html::element( 'span', array( 'class' => 'collection-card-following' ), $followingMsg ) . + Html::element( 'span', array( 'class' => 'collection-card-following' ), '•' ) . + Html::element( 'span', array( 'class' => 'collectoin-card-article-count' ), $articleCountMsg ) . + Html::closeElement( 'div' ) . + Html::closeElement( 'div' ); + return $html; + } +} diff --git a/includes/views/CollectionsListView.php b/includes/views/CollectionsListView.php new file mode 100644 index 0000000..d8f9c60 --- /dev/null +++ b/includes/views/CollectionsListView.php @@ -0,0 +1,56 @@ +<?php +/** + * CollectionsListView.php + */ + +namespace Gather; + +/** + * Renders a mobile collection card list + */ +class CollectionsListView extends Gather\View { + /** + * @param Gather\Collection $collection + */ + public function __construct( Gather\CollectionList $collectionList ) { + $this->collectionList = $collectionList; + } + + /** + * Returns the html for the items of a collection + * + * @param Gather\CollectionList + * + * @return string Html + */ + public static function getListItemsHtml( Gather\CollectionList $collectionList ) { + $html = Html::openElement( 'div', array( 'class' => 'collection-cards' ) ); + foreach ( $collectionList as $item ) { + $view = new Gather\CollectionsListItemCardView( $item ); + $html .= $view->getHtml(); + } + // FIXME: Pagination + $html .= Html::closeElement( 'div' ); + return $html; + } + + /** + * Return title of collection + * + * @return string title for page showing curated lists + */ + public function getTitle() { + return wfMessage( 'gather-lists-title' )->text(); + } + + /** + * @inheritdoc + */ + public function getHtml() { + $html = Html::openElement( 'div', array( 'class' => 'collection content' ) ); + // Get items + $html .= $this->getListItemsHtml( $this->collectionList ); + $html .= Html::closeElement( 'div' ); + return $html; + } +} diff --git a/includes/views/UserNotFoundView.php b/includes/views/UserNotFoundView.php new file mode 100644 index 0000000..f99e085 --- /dev/null +++ b/includes/views/UserNotFoundView.php @@ -0,0 +1,33 @@ + +<?php +/** + * UserNotFoundView.php + */ + +namespace Gather; + +/** + * Renders an error when the user wasn't found + */ +class UserNotFoundView extends Gather\View { + + /** + * @inheritdoc + */ + public function getTitle() { + return wfMessage( 'mobile-frontend-generic-404-title' )->text(); + } + + /** + * @inheritdoc + */ + public function getHtml() { + // FIXME: Showing generic not found error right now. Show user not found instead + $html = Html::openElement( 'div', array( 'class' => 'collection user-not-found' ) ); + $html .= Html::element( 'span', array( 'class' => 'mw-ui-anchor mw-ui-destructive' ), + wfMessage( 'mobile-frontend-generic-404-desc' ) )->text(); + $html .= Html::closeElement( 'div' ); + return $html; + } + +} diff --git a/includes/views/View.php b/includes/views/View.php new file mode 100644 index 0000000..9f0bb85 --- /dev/null +++ b/includes/views/View.php @@ -0,0 +1,36 @@ +<?php +/** + * View.php + */ + +namespace Gather; + +/** + * Render a view. + */ +abstract class View { + /** + * Returns the html for the view + * + * @private + * @return string Html + */ + abstract protected function getHtml(); + + /** + * Returns the title for the view + * + * @private + * @return string Html + */ + abstract protected function getTitle(); + + /** + * Adds HTML of the view to the OutputPage. + * + * @param OutputPage $out + */ + public function render( OutputPage $out ) { + $out->addHTML( $this->getHtml() ); + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..1e8bc0a --- /dev/null +++ b/package.json @@ -0,0 +1,35 @@ +{ + "name": "Gather-dependencies", + "description": "Node.js dependencies used in Gather", + "version": "0.0.1", + "scripts": { + "test": "grunt test" + }, + "repository": { + "type": "git", + "url": "git://git.wikimedia.org/git/mediawiki/extensions/Gather.git" + }, + "dependencies": { + "grunt": "0.4.5", + "grunt-phpdocumentor": "~0.4.1", + "jshint": ">=1.1.0", + "jscs": "git+https://github.com/jscs-dev/node-jscs.git#160c9dc", + "jsdoc": "<=3.3.0", + "kss": ">=0.3.6", + "svgo": ">=0.4.4" + }, + "devDependencies": { + "grunt-contrib-clean": "^0.6.0", + "grunt-mkdir": "^0.1.2", + "grunt-svg2png": "0.2.5", + "grunt-contrib-jshint": "0.10.0", + "grunt-jscs": "git+https://github.com/jdlrobson/grunt-jscs.git#mobilefrontend", + "jscs-jsdoc": "git+https://github.com/jdlrobson/jscs-jsdoc.git#checkParamExistence", + "grunt-jsduck": "^1.0.1", + "js-beautify": "^1.5.4", + "grunt-notify": "^0.3.1", + "grunt-contrib-watch": "0.6.1", + "grunt-lib-phantomjs-istanbul": "0.6.0", + "grunt-qunit-istanbul": "git+https://github.com/joakin/grunt-qunit-istanbul.git#prefix-url" + } +} diff --git a/resources/ext.collections.styles/collections.less b/resources/ext.collections.styles/collections.less new file mode 100644 index 0000000..0eba394 --- /dev/null +++ b/resources/ext.collections.styles/collections.less @@ -0,0 +1,166 @@ +@import "minerva.variables"; +@import "minerva.mixins"; + +.reset-special-page-heading-styles() { + // Reset .mw-mf-special #content_wrapper styles for headers... + // FIXME: Bad defaults. + text-align: left; + font-family: @fontFamilyHeading; + line-height: 1.3em; + margin: 0; +} +.reset-link-styles() { + a, a:visited, a:hover { + color: inherit; + text-decoration: none; + } +} + +// FIXME: Bad special page styling defaults. +#section_0 { + padding-bottom: 0; +} + +.collection { + + // Push the content up to be close to the title + // FIXME: Bad special page styling defaults. + &.content { + padding-top: 0; + } + + /* + * Collection page + */ + + .collection-header { + text-align: center; + padding: 0 1em; + margin-bottom: 2em; + + h1 { + font-weight: bold; + } + + .collection-description { + color: @grayMediumLight; + } + + .collection-owner { + &.mw-ui-icon-before:before { + margin-right: 0.5em; + width: 1em; + } + } + + .collection-action-button { + padding: 0.5em 2em; + } + } + + .collection-empty { + text-align: center; + * { + font-family: @fontFamily; + } + h3 { + padding-bottom: 0em; + } + h6 { + font-size: 0.8em; + } + } + + .collection-items { + } + + .collection-item { + @collectionItemPadding: 1em; + + border: 1px solid @grayLightest; + padding: 0 @collectionItemPadding; + margin-bottom: 2em; + + .collection-item-title { + font-size: 2em; + padding-bottom: 0; + .reset-special-page-heading-styles(); + // FIXME: Why is this inconsistent with other headings colors + color: black; + .reset-link-styles(); + } + + .collection-item-footer { + // Make footer run to edges + margin: 1em -@collectionItemPadding 0; + padding: @collectionItemPadding; + text-align: right; + border-top: 1px solid @grayLightest; + font-weight: bold; + + & * { + // Align the items on the footer + display: inline-block; + vertical-align: bottom; + } + + .collections-read-more-arrow { + // Need a small icon size + width: 1.5em; + min-width: 1.5em; + + &:before { + // FIXME: mw-ui icons assume square/horizontal icons, this svg + // (next.svg) is vertical, so we have to switch the background-size. + // This shouldn't be necessary, either fix the icon(?) or mw-ui to + // support vertical icons. + background-size: auto 100%; + // Fix icon spacing + margin-left: 0.5em; + margin-right: 0; + } + } + } + } + + + /* + * List of collections + */ + + @overlayHeight: 6em; + + .collection-card { + height: @overlayHeight; + border: 1px solid @grayLightest; + position: relative; + } + + .collection-card-overlay { + position: absolute; + height: @overlayHeight; + background: rgba(0, 0, 0, 0.8); + padding: 1em; + bottom: 0; + left: 0; + right: 0; + span { + color: #FAF9F9; + padding: 0 0.25em; + } + } + + .collection-card-title { + font-size: 2em; + padding-bottom: 0; + color: #FAF9F9; + .reset-special-page-heading-styles(); + .reset-link-styles(); + } + + // User not found page + &.user-not-found { + text-align: center; + } + +} diff --git a/resources/ext.collections.styles/icons.less b/resources/ext.collections.styles/icons.less new file mode 100644 index 0000000..e698ed0 --- /dev/null +++ b/resources/ext.collections.styles/icons.less @@ -0,0 +1,9 @@ +// FIXME: This file should be shared across repositories +@import "minerva.variables"; +@import "minerva.mixins"; +@import "mediawiki.mixins"; + +.mw-ui-icon-collections-read-more { + .m-background-image-svg-quick( 'images/icons/next' ); +} + diff --git a/resources/ext.collections.styles/images/icons/next.svg b/resources/ext.collections.styles/images/icons/next.svg new file mode 100644 index 0000000..5b2290e --- /dev/null +++ b/resources/ext.collections.styles/images/icons/next.svg @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 17.5 26" enable-background="new 0 0 17.5 26"> + <g> + <g> + <path fill="#347BFF" d="M4.6 24.3l-3.1-3 8.4-8.4-8.4-8.4 3.1-3L16 12.9z"/> + </g> + </g> +</svg> diff --git a/tests/.htaccess b/tests/.htaccess new file mode 100644 index 0000000..986a40a --- /dev/null +++ b/tests/.htaccess @@ -0,0 +1,4 @@ +Deny from all +<FilesMatch "\.js$"> + Allow from all +</FilesMatch> diff --git a/tests/browser/.gitignore b/tests/browser/.gitignore new file mode 100644 index 0000000..5509614 --- /dev/null +++ b/tests/browser/.gitignore @@ -0,0 +1,5 @@ +# Puppet-managed dependencies for browser tests +.bundle +.gem +.ruby-version +screenshots -- To view, visit https://gerrit.wikimedia.org/r/189157 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: I82d787b4cfbd2c704db7680859c71e0e2d3f4c94 Gerrit-PatchSet: 7 Gerrit-Project: mediawiki/extensions/Gather Gerrit-Branch: master Gerrit-Owner: Robmoen <[email protected]> Gerrit-Reviewer: Alex Monk <[email protected]> Gerrit-Reviewer: Jdlrobson <[email protected]> Gerrit-Reviewer: Jhernandez <[email protected]> Gerrit-Reviewer: Legoktm <[email protected]> Gerrit-Reviewer: MaxSem <[email protected]> Gerrit-Reviewer: Robmoen <[email protected]> Gerrit-Reviewer: Siebrand <[email protected]> _______________________________________________ MediaWiki-commits mailing list [email protected] https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits
