├── .nvmrc ├── .github ├── CODEOWNERS ├── labeler.yml └── workflows │ └── changelog.yml ├── .prettierignore ├── commitlint.config.js ├── .stylelintrc.js ├── README.md ├── babel.config.js ├── phpcsSniffs ├── ruleset.xml └── Sniffs │ └── NewspackNewslettersMethodsSniff.php ├── .prettierrc.js ├── src ├── post-primary-brand │ ├── index.scss │ └── index.js ├── admin │ ├── index.scss │ ├── index.js │ └── views │ │ └── brands │ │ ├── style.scss │ │ ├── BrandsList.js │ │ ├── index.js │ │ └── Brand.js └── prompt-brands │ └── index.js ├── .eslintrc.js ├── .gitignore ├── phpunit.xml.dist ├── .distignore ├── webpack.config.js ├── composer.json ├── includes ├── customizations │ ├── class-blogname.php │ ├── class-site-title.php │ ├── class-body-class.php │ ├── class-menus.php │ ├── class-logo.php │ ├── class-popups-should-display-prompt.php │ ├── class-url.php │ ├── class-theme-colors.php │ └── class-show-page-on-front.php ├── class-initializer.php ├── integrations │ └── class-google-analytics.php ├── meta │ ├── class-logo.php │ ├── class-show-page-on-front.php │ ├── class-url.php │ ├── class-user-primary-brand.php │ ├── class-tag-primary-brand.php │ ├── class-category-primary-brand.php │ ├── class-menus.php │ ├── class-theme-colors.php │ └── class-post-primary-brand.php ├── admin │ ├── class-prompt-popups.php │ ├── class-show-page-on-front.php │ ├── class-filter-posts.php │ ├── class-post-primary-brand.php │ ├── class-cat-primary-brand.php │ └── class-user-primary-brand.php ├── class-meta.php ├── class-admin.php └── class-taxonomy.php ├── phpunit.xml ├── tests ├── unit-tests │ ├── test-customization-logo.php │ ├── test-customization-url.php │ ├── test-customization-menus.php │ ├── test-meta.php │ ├── test-rest-url.php │ ├── test-rest-logo.php │ ├── test-rest-menus.php │ ├── test-customization-page-on-front.php │ ├── test-rest-page-on-front.php │ ├── test-rest-theme-colors.php │ ├── test-customization-theme-colors.php │ ├── test-rest-post-primary-brand.php │ ├── test-customization-should-display-prompt.php │ └── test-taxonomy.php ├── bootstrap.php └── class-newspack-multibranded-rest-testcase.php ├── .circleci └── config.yml ├── newspack-multibranded-site.php ├── phpcs.xml ├── .travis.yml ├── .phpcs.xml.dist ├── package.json ├── bin └── install-wp-tests.sh ├── languages └── newspack-multibranded-site.pot └── CHANGELOG.md /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/* -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @Automattic/newspack-product 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | release 4 | vendor -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | needs-changelog: 2 | - base-branch: ['trunk'] 3 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: [ '@commitlint/config-conventional' ] }; 2 | -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ './node_modules/newspack-scripts/config/stylelint.config.js' ], 3 | }; 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Newspack Multibranded Site plugin 2 | 3 | Brand different content and sections of your site with unique colors and navigation. 4 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = api => { 2 | api.cache( true ); 3 | return { 4 | extends: 'newspack-scripts/config/babel.config.js', 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /phpcsSniffs/ruleset.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Add Newspack specific rules. 4 | 5 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | const baseConfig = require( './node_modules/newspack-scripts/config/prettier.config.js' ); 2 | 3 | module.exports = { 4 | ...baseConfig 5 | }; 6 | -------------------------------------------------------------------------------- /src/post-primary-brand/index.scss: -------------------------------------------------------------------------------- 1 | .newspack-multibranded-site-brand-control { 2 | .editor-post-taxonomies__hierarchical-terms-add { 3 | display: none; 4 | } 5 | .editor-primary-brand-selector { 6 | margin-top: 8px; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ './node_modules/newspack-scripts/.eslintrc.js' ], 3 | ignorePatterns: [ 'dist/', 'node_modules/' ], 4 | rules: { 5 | 'no-nested-ternary': 'off', 6 | 'react/display-name': 'off', 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Editors 2 | project.xml 3 | project.properties 4 | /nbproject/private/ 5 | .buildpath 6 | .project 7 | .settings* 8 | .idea 9 | .vscode 10 | *.sublime-project 11 | *.sublime-workspace 12 | .sublimelinterrc 13 | 14 | # OS X metadata 15 | .DS_Store 16 | 17 | # Windows junk 18 | Thumbs.db 19 | 20 | # Composer 21 | /vendor/ 22 | /node_modules/ 23 | /dist/ 24 | .DS_Store 25 | release 26 | .cache 27 | 28 | # Tests 29 | codecov/ 30 | coverage/ 31 | .phpunit.result.cache 32 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | ./tests/ 13 | ./tests/test-sample.php 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /.distignore: -------------------------------------------------------------------------------- 1 | # A set of files you probably don't want in your WordPress.org distribution 2 | 3 | .*ignore 4 | .*rc\* 5 | _.config.js 6 | .git 7 | .github 8 | .cache 9 | .vscode 10 | .circleci 11 | .DS_Store 12 | Thumbs.db 13 | composer.json 14 | composer.lock 15 | package.json 16 | package-lock.json 17 | yarn.lock 18 | phpunit.xml 19 | phpunit.xml.dist 20 | .phpcs.xml 21 | phpcs.xml 22 | .phpcs.xml.dist 23 | phpcs.xml.dist 24 | README.md 25 | PULL_REQUEST_TEMPLATE.md 26 | webpack.config.js 27 | _.sql 28 | _.tar.gz 29 | _.zip 30 | node_modules 31 | /tests 32 | /bin 33 | /src 34 | -------------------------------------------------------------------------------- /src/admin/index.scss: -------------------------------------------------------------------------------- 1 | body { 2 | background: white; 3 | margin: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | 9 | #wpcontent { 10 | padding-left: 0; 11 | } 12 | 13 | #wpbody-content { 14 | min-height: calc(100vh - 38px); 15 | 16 | > .notice, 17 | > .error { 18 | + #root { 19 | border-top: 1px solid #ddd; 20 | margin-top: 32px; 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/admin/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { HashRouter as Router } from 'react-router-dom'; 3 | 4 | import { render, createElement, Component } from '@wordpress/element'; 5 | import domReady from '@wordpress/dom-ready'; 6 | 7 | import './index.scss'; 8 | import Brands from './views/brands'; 9 | 10 | class App extends Component { 11 | render() { 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | } 20 | } 21 | 22 | domReady( () => { 23 | render( createElement( App ), document.getElementById( 'root' ) ); 24 | } ); 25 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | **** WARNING: No ES6 modules here. Not transpiled! **** 3 | */ 4 | /* eslint-disable import/no-nodejs-modules */ 5 | /* eslint-disable @typescript-eslint/no-var-requires */ 6 | 7 | /** 8 | * External dependencies 9 | */ 10 | const getBaseWebpackConfig = require( 'newspack-scripts/config/getWebpackConfig' ); 11 | const path = require( 'path' ); 12 | 13 | /** 14 | * Internal variables 15 | */ 16 | const entry = { 17 | admin: path.join( __dirname, 'src/admin' ), 18 | postPrimaryBrand: path.join( __dirname, 'src/post-primary-brand' ), 19 | promptBrands: path.join( __dirname, 'src/prompt-brands' ), 20 | }; 21 | 22 | const webpackConfig = getBaseWebpackConfig( { 23 | entry, 24 | } ); 25 | 26 | module.exports = webpackConfig; 27 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "automattic/newspack-multibranded-site", 3 | "description": "A plugin to allow your site to host multiple brands", 4 | "type": "wordpress-plugin", 5 | "license": "GPL-3.0", 6 | "require-dev": { 7 | "automattic/vipwpcs": "^3.0", 8 | "wp-coding-standards/wpcs": "^3.0", 9 | "dealerdirect/phpcodesniffer-composer-installer": "*", 10 | "phpcompatibility/phpcompatibility-wp": "*", 11 | "yoast/phpunit-polyfills": "^2.0", 12 | "phpunit/phpunit": "^9.5" 13 | }, 14 | "autoload": { 15 | "classmap": [ 16 | "./includes" 17 | ] 18 | }, 19 | "config": { 20 | "platform": { 21 | "php": "8.3" 22 | }, 23 | "allow-plugins": { 24 | "composer/installers": true, 25 | "dealerdirect/phpcodesniffer-composer-installer": true 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /includes/customizations/class-blogname.php: -------------------------------------------------------------------------------- 1 | name; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /includes/class-initializer.php: -------------------------------------------------------------------------------- 1 | div, 15 | .components-radio-control__group-wrapper { 16 | justify-content: flex-start; 17 | flex-direction: row; 18 | 19 | .components-radio-control__option { 20 | margin-right: 20px; 21 | 22 | &:last-child { 23 | margin-right: 0; 24 | } 25 | } 26 | } 27 | } 28 | 29 | &__base-url-component { 30 | display: flex; 31 | align-items: baseline; 32 | } 33 | 34 | &__theme-mod-color-picker { 35 | .newspack-color-picker__label { 36 | display: flex; 37 | justify-content: space-between; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /includes/customizations/class-site-title.php: -------------------------------------------------------------------------------- 1 | name; 33 | return $params; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /includes/customizations/class-body-class.php: -------------------------------------------------------------------------------- 1 | slug}"; 37 | 38 | return $classes; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./includes 6 | 7 | 8 | ./node_modules 9 | ./tests 10 | ./vendor 11 | 12 | 13 | 14 | 15 | ./tests/unit-tests 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /includes/meta/class-logo.php: -------------------------------------------------------------------------------- 1 | 'integer', 43 | 'nullable' => true, 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /includes/meta/class-show-page-on-front.php: -------------------------------------------------------------------------------- 1 | 'integer', 43 | 'nullable' => true, 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /includes/admin/class-prompt-popups.php: -------------------------------------------------------------------------------- 1 | base ) { 31 | return; 32 | } 33 | 34 | $asset = require NEWSPACK_MULTIBRANDED_SITE_PLUGIN_DIR . '/dist/promptBrands.asset.php'; 35 | 36 | wp_enqueue_script( 37 | 'newspack-prompt-brands', 38 | plugins_url( '../../dist/promptBrands.js', __FILE__ ), 39 | $asset['dependencies'], 40 | $asset['version'], 41 | true 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /includes/meta/class-url.php: -------------------------------------------------------------------------------- 1 | 'string', 43 | 'enum' => [ 'yes', 'no' ], 44 | 'nullable' => false, 45 | 'default' => 'no', 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/unit-tests/test-customization-logo.php: -------------------------------------------------------------------------------- 1 | factory->term->create_and_get( [ 'taxonomy' => Taxonomy::SLUG ] ); 21 | $term_with_custom_logo = $this->factory->term->create_and_get( [ 'taxonomy' => Taxonomy::SLUG ] ); 22 | add_term_meta( $term_with_custom_logo->term_id, Logo::get_key(), 123 ); 23 | 24 | $this->go_to( get_term_link( $term_without_custom_logo->term_id ) ); 25 | $this->assertSame( false, get_theme_mod( 'custom_logo' ) ); 26 | 27 | $this->go_to( get_term_link( $term_with_custom_logo->term_id ) ); 28 | $this->assertSame( '123', get_theme_mod( 'custom_logo' ) ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /includes/meta/class-user-primary-brand.php: -------------------------------------------------------------------------------- 1 | 'integer', 51 | 'nullable' => true, 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /includes/meta/class-tag-primary-brand.php: -------------------------------------------------------------------------------- 1 | 'integer', 53 | 'nullable' => true, 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/unit-tests/test-customization-url.php: -------------------------------------------------------------------------------- 1 | factory->term->create_and_get( [ 'taxonomy' => Taxonomy::SLUG ] ); 21 | $term_with_custom_url = $this->factory->term->create_and_get( [ 'taxonomy' => Taxonomy::SLUG ] ); 22 | add_term_meta( $term_with_custom_url->term_id, Url_Meta::get_key(), 'yes' ); 23 | 24 | $this->set_permalink_structure( '/%postname%/' ); 25 | 26 | $this->go_to( home_url( $term_without_custom_url->slug ) ); 27 | $this->assertFalse( is_home() ); 28 | $this->assertTrue( is_404() ); 29 | 30 | $this->go_to( home_url( $term_with_custom_url->slug ) ); 31 | $this->assertFalse( is_home() ); 32 | $this->assertTrue( is_tax() ); 33 | $this->assertSame( $term_with_custom_url->term_id, get_queried_object_id() ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /includes/meta/class-category-primary-brand.php: -------------------------------------------------------------------------------- 1 | 'integer', 53 | 'nullable' => true, 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /includes/meta/class-menus.php: -------------------------------------------------------------------------------- 1 | 'array', 43 | 'nullable' => true, 44 | 'items' => [ 45 | 'type' => 'object', 46 | 'properties' => [ 47 | 'location' => [ 48 | 'type' => 'string', 49 | 'nullable' => false, 50 | ], 51 | 'menu' => [ 52 | 'type' => 'integer', 53 | 'nullable' => false, 54 | ], 55 | ], 56 | ], 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | newspack: newspack/newspack@1.5.8 5 | 6 | workflows: 7 | version: 2 8 | all: 9 | jobs: 10 | - newspack/build 11 | - newspack/i18n: 12 | requires: 13 | - newspack/build 14 | filters: 15 | branches: 16 | only: 17 | - trunk 18 | - newspack/lint-js-scss: 19 | requires: 20 | - newspack/build 21 | - newspack/release: 22 | requires: 23 | - newspack/build 24 | filters: 25 | branches: 26 | only: 27 | - release 28 | - alpha 29 | - /^hotfix\/.*/ 30 | - /^epic\/.*/ 31 | - newspack/build-distributable: 32 | requires: 33 | - newspack/build 34 | # Running this after release ensure the version number in files will be correct. 35 | - newspack/release 36 | archive-name: 'newspack-multibranded-site' 37 | - newspack/post-release: 38 | requires: 39 | - newspack/release 40 | filters: 41 | branches: 42 | only: 43 | - release 44 | php: 45 | jobs: 46 | - newspack/lint-php 47 | - newspack/test-php 48 | -------------------------------------------------------------------------------- /tests/unit-tests/test-customization-menus.php: -------------------------------------------------------------------------------- 1 | factory->term->create_and_get( [ 'taxonomy' => Taxonomy::SLUG ] ); 21 | $term_with_custom_menus = $this->factory->term->create_and_get( [ 'taxonomy' => Taxonomy::SLUG ] ); 22 | 23 | set_theme_mod( 'nav_menu_locations', [ 'primary' => 999 ] ); 24 | 25 | add_term_meta( 26 | $term_with_custom_menus->term_id, 27 | Menus::get_key(), 28 | [ 29 | [ 30 | 'location' => 'primary', 31 | 'menu' => 123, 32 | ], 33 | ] 34 | ); 35 | 36 | $this->go_to( get_term_link( $term_without_custom_menus->term_id ) ); 37 | $logos = get_theme_mod( 'nav_menu_locations' ); 38 | $this->assertSame( 999, $logos['primary'] ); 39 | 40 | $this->go_to( get_term_link( $term_with_custom_menus->term_id ) ); 41 | $logos = get_theme_mod( 'nav_menu_locations' ); 42 | $this->assertSame( 123, $logos['primary'] ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/prompt-brands/index.js: -------------------------------------------------------------------------------- 1 | import { addFilter } from '@wordpress/hooks'; 2 | 3 | addFilter( 'newspack.wizards.campaigns.conflictingPrompts', 'newspack/multibranded-site/brand-selector-filter', ( conflicts, prompt ) => { 4 | // If there are conflicting prompts, see if they are for different brands, in which case there will be no conflict. 5 | if ( conflicts.length ) { 6 | const promptBrandsIds = prompt.brand.length ? prompt.brand.map( b => b.term_id ) : []; 7 | 8 | if ( 0 === promptBrandsIds.length ) { 9 | // If the current prompt is not assigned to any brands, the conflicts will remain because 10 | // this prompt will be displayed in all pages, including brand pages. 11 | return conflicts; 12 | } 13 | 14 | const conflictingBrands = conflicts.filter( conflict => { 15 | if ( conflict.brand.length ) { 16 | let stillHasConflict = false; 17 | conflict.brand.forEach( brand => { 18 | // if the prompt has a brand that is also in the conflicting prompt, they are still conflicting. 19 | if ( promptBrandsIds.includes( brand.term_id ) ) { 20 | stillHasConflict = true; 21 | } 22 | } ); 23 | return stillHasConflict; 24 | } 25 | // if current prompt has a brand and conflicting prompt has no brand, they are still conflicting. 26 | return true; 27 | } ); 28 | return conflictingBrands; 29 | } 30 | return conflicts; 31 | } ); 32 | -------------------------------------------------------------------------------- /includes/meta/class-theme-colors.php: -------------------------------------------------------------------------------- 1 | 'array', 43 | 'items' => [ 44 | 'type' => 'object', 45 | 'properties' => [ 46 | 'name' => [ 47 | 'type' => 'string', 48 | ], 49 | 'color' => [ 50 | 'type' => 'string', 51 | 'maxLength' => 7, 52 | 'pattern' => '^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$', 53 | ], 54 | ], 55 | ], 56 | 'nullable' => true, 57 | 'default' => [], 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /includes/meta/class-post-primary-brand.php: -------------------------------------------------------------------------------- 1 | 'integer', 60 | 'nullable' => true, 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /includes/customizations/class-menus.php: -------------------------------------------------------------------------------- 1 | term_id, Menus_Meta::get_key(), true ); 38 | if ( empty( $custom_menus ) ) { 39 | return $nav_menu_locations; 40 | } 41 | foreach ( $custom_menus as $custom_menu ) { 42 | $custom_menu = (array) $custom_menu; 43 | if ( empty( $custom_menu['menu'] ) ) { 44 | continue; 45 | } 46 | $nav_menu_locations[ $custom_menu['location'] ] = $custom_menu['menu']; 47 | } 48 | 49 | return $nav_menu_locations; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/unit-tests/test-meta.php: -------------------------------------------------------------------------------- 1 | assertSame( 'edit_users', Newspack_Multibranded_Site\Meta\User_Primary_Brand::get_capability() ); 18 | $this->assertSame( 'edit_post', Newspack_Multibranded_Site\Meta\Post_Primary_Brand::get_capability() ); 19 | 20 | $this->assertSame( get_taxonomy( 'category' )->cap->manage_terms, Newspack_Multibranded_Site\Meta\Category_Primary_Brand::get_capability() ); 21 | $this->assertSame( get_taxonomy( 'post_tag' )->cap->manage_terms, Newspack_Multibranded_Site\Meta\Tag_Primary_Brand::get_capability() ); 22 | 23 | $brand_tax_cap = get_taxonomy( Newspack_Multibranded_Site\Taxonomy::SLUG )->cap->manage_terms; 24 | 25 | $this->assertSame( $brand_tax_cap, Newspack_Multibranded_Site\Meta\Logo::get_capability() ); 26 | $this->assertSame( $brand_tax_cap, Newspack_Multibranded_Site\Meta\Menus::get_capability() ); 27 | $this->assertSame( $brand_tax_cap, Newspack_Multibranded_Site\Meta\Show_Page_On_Front::get_capability() ); 28 | $this->assertSame( $brand_tax_cap, Newspack_Multibranded_Site\Meta\Theme_Colors::get_capability() ); 29 | $this->assertSame( $brand_tax_cap, Newspack_Multibranded_Site\Meta\Url::get_capability() ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/changelog.yml: -------------------------------------------------------------------------------- 1 | name: "Pull Request Labeler" 2 | on: 3 | pull_request: 4 | types: [closed] 5 | 6 | jobs: 7 | labeler: 8 | if: github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'trunk' && github.event.pull_request.user.login != 'dependabot[bot]' 9 | permissions: 10 | contents: read 11 | pull-requests: write 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/labeler@v5 15 | 16 | comment_pr: 17 | if: github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'trunk' && github.event.pull_request.user.login != 'dependabot[bot]' 18 | permissions: 19 | contents: read 20 | pull-requests: write 21 | runs-on: ubuntu-latest 22 | name: Comment about the change log label 23 | steps: 24 | - name: Comment PR 25 | uses: thollander/actions-comment-pull-request@v3 26 | with: 27 | message: | 28 | Hey @${{ github.event.pull_request.user.login }}, good job getting this PR merged! :tada: 29 | 30 | Now, the `needs-changelog` label has been added to it. 31 | 32 | Please check if this PR needs to be included in the "Upcoming Changes" and "Release Notes" doc. If it doesn't, simply remove the label. 33 | 34 | If it does, please add an entry to our shared document, with screenshots and testing instructions if applicable, then remove the label. 35 | 36 | Thank you! :heart: 37 | -------------------------------------------------------------------------------- /includes/customizations/class-logo.php: -------------------------------------------------------------------------------- 1 | term_id, Logo_Meta::get_key(), true ); 38 | if ( $custom_logo ) { 39 | $logo_id = $custom_logo; 40 | } 41 | return $logo_id; 42 | } 43 | 44 | /** 45 | * Filters the html output of the custom logo 46 | * 47 | * @param string $html The custom logo html. 48 | * @return string 49 | */ 50 | public static function get_custom_logo( $html ) { 51 | $brand = Taxonomy::get_current(); 52 | if ( ! $brand ) { 53 | return $html; 54 | } 55 | 56 | $html = preg_replace( '|href="[^"]+"|', 'href="' . get_term_link( $brand ) . '"', $html ); 57 | 58 | return $html; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /includes/admin/class-show-page-on-front.php: -------------------------------------------------------------------------------- 1 | post_type ) { 35 | return $post_states; 36 | } 37 | 38 | $brand = Show_Page_On_Front_Customization::get_brand_page_is_cover_for( $post->ID ); 39 | if ( ! $brand ) { 40 | return $post_states; 41 | } 42 | 43 | $brand = get_term( $brand, Taxonomy::SLUG ); 44 | if ( $brand instanceof WP_Term ) { 45 | $post_states['newspack-front-page'] = sprintf( 46 | /* translators: %s: Brand name */ 47 | __( 'Front page for %s', 'newspack-multibranded-site' ), 48 | $brand->name 49 | ); 50 | } 51 | return $post_states; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /newspack-multibranded-site.php: -------------------------------------------------------------------------------- 1 | dispatch_request_to_edit_termmeta( Url::get_key(), 'yes' ); 17 | $this->assertSame( 401, $response->get_status() ); 18 | 19 | wp_set_current_user( $this->secondary_user_id->ID ); 20 | $response = $this->dispatch_request_to_edit_termmeta( Url::get_key(), 'yes' ); 21 | $this->assertSame( 403, $response->get_status() ); 22 | } 23 | 24 | /** 25 | * Test setting a valid value 26 | */ 27 | public function test_valid_input() { 28 | wp_set_current_user( $this->user_id->ID ); 29 | $response = $this->dispatch_request_to_edit_termmeta( Url::get_key(), 'yes' ); 30 | $data = $response->get_data(); 31 | 32 | $this->assertSame( 200, $response->get_status() ); 33 | $this->assertSame( 'yes', $data['meta'][ Url::get_key() ] ); 34 | } 35 | 36 | /** 37 | * Test setting an invalid value 38 | */ 39 | public function test_invalid_input() { 40 | wp_set_current_user( $this->user_id->ID ); 41 | $response = $this->dispatch_request_to_edit_termmeta( Url::get_key(), 'invalid' ); 42 | $data = $response->get_data(); 43 | $this->assertSame( 400, $response->get_status() ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/unit-tests/test-rest-logo.php: -------------------------------------------------------------------------------- 1 | dispatch_request_to_edit_termmeta( Logo::get_key(), 123 ); 18 | $this->assertSame( 401, $response->get_status() ); 19 | 20 | wp_set_current_user( $this->secondary_user_id->ID ); 21 | $response = $this->dispatch_request_to_edit_termmeta( Logo::get_key(), 123 ); 22 | $this->assertSame( 403, $response->get_status() ); 23 | } 24 | 25 | /** 26 | * Test setting a valid value 27 | */ 28 | public function test_valid_input() { 29 | wp_set_current_user( $this->user_id->ID ); 30 | $response = $this->dispatch_request_to_edit_termmeta( Logo::get_key(), 123 ); 31 | $data = $response->get_data(); 32 | 33 | $this->assertSame( 200, $response->get_status() ); 34 | $this->assertSame( 123, $data['meta'][ Logo::get_key() ] ); 35 | } 36 | 37 | /** 38 | * Test setting an invalid value 39 | */ 40 | public function test_invalid_input() { 41 | wp_set_current_user( $this->user_id->ID ); 42 | $response = $this->dispatch_request_to_edit_termmeta( Logo::get_key(), 'invalid' ); 43 | $data = $response->get_data(); 44 | $this->assertSame( 400, $response->get_status() ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /includes/customizations/class-popups-should-display-prompt.php: -------------------------------------------------------------------------------- 1 | term_id, $popup_term_ids, true ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | 'primary_color', 37 | 'label' => 'Primary Color', 38 | 'default' => '#00669b', 39 | ], 40 | ]; 41 | } 42 | ); 43 | 44 | require_once __DIR__ . '/../vendor/yoast/phpunit-polyfills/phpunitpolyfills-autoload.php'; 45 | 46 | // Start up the WP testing environment. 47 | require "{$newspack_multibranded_site_test_dir}/includes/bootstrap.php"; 48 | 49 | require __DIR__ . '/class-newspack-multibranded-rest-testcase.php'; 50 | -------------------------------------------------------------------------------- /includes/admin/class-filter-posts.php: -------------------------------------------------------------------------------- 1 | Taxonomy::SLUG, 40 | 'hide_empty' => true, 41 | ] 42 | ); 43 | // If we have no brands or no posts with a brand, then don't show the dropdown. 44 | if ( $num_posts_with_brand < 1 ) { 45 | return; 46 | } 47 | 48 | $taxonomy_object = get_taxonomy( Taxonomy::SLUG ); 49 | $selected = isset( $_GET[ Taxonomy::SLUG ] ) ? sanitize_text_field( wp_unslash( $_GET[ Taxonomy::SLUG ] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended 50 | 51 | wp_dropdown_categories( 52 | array( 53 | 'show_option_all' => $taxonomy_object->labels->all_items, 54 | 'taxonomy' => Taxonomy::SLUG, 55 | 'name' => Taxonomy::SLUG, 56 | 'orderby' => 'name', 57 | 'value_field' => 'slug', 58 | 'selected' => $selected, 59 | 'hierarchical' => false, 60 | ) 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Generally-applicable sniffs for WordPress plugins 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | . 42 | 43 | */dev-lib/* 44 | */node_modules/* 45 | */vendor/* 46 | */dist/* 47 | */release/* 48 | 49 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | dist: trusty 3 | 4 | language: php 5 | 6 | notifications: 7 | email: 8 | on_success: never 9 | on_failure: change 10 | 11 | branches: 12 | only: 13 | - trunk 14 | 15 | cache: 16 | directories: 17 | - $HOME/.composer/cache 18 | 19 | matrix: 20 | include: 21 | - php: 7.4 22 | env: WP_VERSION=latest 23 | - php: 7.3 24 | env: WP_VERSION=latest 25 | - php: 7.2 26 | env: WP_VERSION=latest 27 | - php: 7.1 28 | env: WP_VERSION=latest 29 | - php: 7.0 30 | env: WP_VERSION=latest 31 | - php: 5.6 32 | env: WP_VERSION=latest 33 | - php: 5.6 34 | env: WP_VERSION=trunk 35 | - php: 5.6 36 | env: WP_TRAVISCI=phpcs 37 | 38 | before_script: 39 | - export PATH="$HOME/.composer/vendor/bin:$PATH" 40 | - | 41 | if [ -f ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini ]; then 42 | phpenv config-rm xdebug.ini 43 | else 44 | echo "xdebug.ini does not exist" 45 | fi 46 | - | 47 | if [[ ! -z "$WP_VERSION" ]] ; then 48 | bash bin/install-wp-tests.sh wordpress_test root '' localhost $WP_VERSION 49 | composer global require "phpunit/phpunit=4.8.*|5.7.*" 50 | fi 51 | - | 52 | if [[ "$WP_TRAVISCI" == "phpcs" ]] ; then 53 | composer global require wp-coding-standards/wpcs 54 | composer global require phpcompatibility/php-compatibility 55 | composer global require phpcompatibility/phpcompatibility-paragonie 56 | composer global require phpcompatibility/phpcompatibility-wp 57 | phpcs --config-set installed_paths $HOME/.composer/vendor/wp-coding-standards/wpcs,$HOME/.composer/vendor/phpcompatibility/php-compatibility,$HOME/.composer/vendor/phpcompatibility/phpcompatibility-paragonie,$HOME/.composer/vendor/phpcompatibility/phpcompatibility-wp 58 | fi 59 | 60 | script: 61 | - | 62 | if [[ ! -z "$WP_VERSION" ]] ; then 63 | phpunit 64 | WP_MULTISITE=1 phpunit 65 | fi 66 | - | 67 | if [[ "$WP_TRAVISCI" == "phpcs" ]] ; then 68 | phpcs 69 | fi 70 | -------------------------------------------------------------------------------- /tests/unit-tests/test-rest-menus.php: -------------------------------------------------------------------------------- 1 | 'primary', 20 | 'menu' => 1, 21 | ], 22 | [ 23 | 'location' => 'secondary', 24 | 'menu' => 2, 25 | ], 26 | ]; 27 | } 28 | 29 | /** 30 | * Test editing without permissions 31 | */ 32 | public function test_unauthorized() { 33 | wp_set_current_user( 0 ); 34 | $response = $this->dispatch_request_to_edit_termmeta( Menus::get_key(), $this->get_valid_input() ); 35 | $this->assertSame( 401, $response->get_status() ); 36 | 37 | wp_set_current_user( $this->secondary_user_id->ID ); 38 | $response = $this->dispatch_request_to_edit_termmeta( Menus::get_key(), $this->get_valid_input() ); 39 | $this->assertSame( 403, $response->get_status() ); 40 | } 41 | 42 | /** 43 | * Test setting a valid value 44 | */ 45 | public function test_valid_input() { 46 | wp_set_current_user( $this->user_id->ID ); 47 | $response = $this->dispatch_request_to_edit_termmeta( Menus::get_key(), $this->get_valid_input() ); 48 | $data = $response->get_data(); 49 | 50 | $this->assertSame( 200, $response->get_status() ); 51 | $this->assertSame( $this->get_valid_input(), $data['meta'][ Menus::get_key() ] ); 52 | } 53 | 54 | /** 55 | * Test setting an invalid value 56 | */ 57 | public function test_invalid_input() { 58 | wp_set_current_user( $this->user_id->ID ); 59 | $response = $this->dispatch_request_to_edit_termmeta( Menus::get_key(), 'invalid' ); 60 | $data = $response->get_data(); 61 | $this->assertSame( 400, $response->get_status() ); 62 | 63 | $response = $this->dispatch_request_to_edit_termmeta( 64 | Menus::get_key(), 65 | [ 66 | [ 67 | 'location' => 'asd', 68 | 'menu' => false, 69 | ], 70 | ] 71 | ); 72 | $data = $response->get_data(); 73 | 74 | $this->assertSame( 400, $response->get_status() ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tests/unit-tests/test-customization-page-on-front.php: -------------------------------------------------------------------------------- 1 | factory->term->create_and_get( array( 'taxonomy' => Taxonomy::SLUG ) ); 22 | add_term_meta( $brand_with_page_on_front->term_id, Show_Page_On_Front_Meta::get_key(), 123 ); 23 | $this->assertSame( $brand_with_page_on_front->term_id, Show_Page_On_Front::get_brand_page_is_cover_for( 123 ) ); 24 | 25 | add_term_meta( $brand_with_page_on_front->term_id, Show_Page_On_Front_Meta::get_key(), 456 ); 26 | $this->assertSame( $brand_with_page_on_front->term_id, Show_Page_On_Front::get_brand_page_is_cover_for( 456 ) ); 27 | $this->assertNull( Show_Page_On_Front::get_brand_page_is_cover_for( 123 ) ); 28 | 29 | update_term_meta( $brand_with_page_on_front->term_id, Show_Page_On_Front_Meta::get_key(), 0 ); 30 | $this->assertNull( Show_Page_On_Front::get_brand_page_is_cover_for( 123 ) ); 31 | $this->assertNull( Show_Page_On_Front::get_brand_page_is_cover_for( 456 ) ); 32 | } 33 | 34 | /** 35 | * Ensure that front page filter is not applied when visiting the feed for the brand 36 | */ 37 | public function test_rss_feed_intact() { 38 | $brand_with_page_on_front = $this->factory->term->create_and_get( array( 'taxonomy' => Taxonomy::SLUG ) ); 39 | 40 | $page2 = $this->factory->post->create_and_get( 41 | array( 42 | 'post_title' => 'Page 2', 43 | 'post_type' => 'page', 44 | ) 45 | ); 46 | add_term_meta( $brand_with_page_on_front->term_id, Show_Page_On_Front_Meta::get_key(), $page2->ID ); 47 | 48 | $this->go_to( get_term_link( $brand_with_page_on_front ) ); 49 | $this->assertTrue( Show_Page_On_Front::is_filtered() ); 50 | 51 | $this->go_to( get_term_link( $brand_with_page_on_front ) . '&feed=rss' ); 52 | $this->assertFalse( Show_Page_On_Front::is_filtered() ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/unit-tests/test-rest-page-on-front.php: -------------------------------------------------------------------------------- 1 | dispatch_request_to_edit_termmeta( Show_Page_On_Front::get_key(), 'yes' ); 18 | $this->assertSame( 401, $response->get_status() ); 19 | 20 | wp_set_current_user( $this->secondary_user_id->ID ); 21 | $response = $this->dispatch_request_to_edit_termmeta( Show_Page_On_Front::get_key(), 'yes' ); 22 | $this->assertSame( 403, $response->get_status() ); 23 | } 24 | 25 | /** 26 | * Test setting a valid value 27 | */ 28 | public function test_valid_input() { 29 | wp_set_current_user( $this->user_id->ID ); 30 | $response = $this->dispatch_request_to_edit_termmeta( Show_Page_On_Front::get_key(), 2 ); 31 | $data = $response->get_data(); 32 | 33 | $this->assertSame( 200, $response->get_status() ); 34 | $this->assertSame( 2, $data['meta'][ Show_Page_On_Front::get_key() ] ); 35 | } 36 | 37 | /** 38 | * Test deleting the meta value 39 | */ 40 | public function test_delete() { 41 | wp_set_current_user( $this->user_id->ID ); 42 | $response = $this->dispatch_request_to_edit_termmeta( Show_Page_On_Front::get_key(), 2 ); 43 | 44 | $response = $this->dispatch_request_to_edit_termmeta( Show_Page_On_Front::get_key(), null ); 45 | $data = $response->get_data(); 46 | 47 | $this->assertSame( 200, $response->get_status() ); 48 | $this->assertSame( 0, $data['meta'][ Show_Page_On_Front::get_key() ] ); 49 | $this->assertEmpty( get_term_meta( $this->term1->term_id, Show_Page_On_Front::get_key(), true ) ); 50 | } 51 | 52 | /** 53 | * Test setting an invalid value 54 | */ 55 | public function test_invalid_input() { 56 | wp_set_current_user( $this->user_id->ID ); 57 | $response = $this->dispatch_request_to_edit_termmeta( Show_Page_On_Front::get_key(), 'asd' ); 58 | $data = $response->get_data(); 59 | $this->assertSame( 400, $response->get_status() ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /.phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | Generally-applicable sniffs for WordPress plugins. 4 | 5 | 6 | . 7 | /vendor/ 8 | /node_modules/ 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /includes/customizations/class-url.php: -------------------------------------------------------------------------------- 1 | matched_query ); 34 | 35 | if ( empty( $matched_query['pagename'] ) && empty( $matched_query['name'] ) ) { 36 | return; 37 | } 38 | 39 | $pagename = $matched_query['pagename'] ?? $matched_query['name']; 40 | 41 | $terms = get_terms( 42 | array( 43 | 'taxonomy' => Taxonomy::SLUG, 44 | 'hide_empty' => false, 45 | 'meta_key' => Url_Meta::get_key(), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value 46 | 'meta_value' => 'yes', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value 47 | ) 48 | ); 49 | 50 | foreach ( $terms as $term ) { 51 | if ( $term->slug === $pagename ) { 52 | if ( isset( $wp->query_vars['name'] ) ) { 53 | unset( $wp->query_vars['name'] ); 54 | } 55 | if ( isset( $wp->query_vars['pagename'] ) ) { 56 | unset( $wp->query_vars['pagename'] ); 57 | } 58 | if ( isset( $wp->query_vars['page'] ) ) { 59 | unset( $wp->query_vars['page'] ); 60 | } 61 | 62 | $wp->query_vars[ Taxonomy::SLUG ] = $term->slug; 63 | break; 64 | } 65 | } 66 | } 67 | 68 | /** 69 | * Make sure the term link is the slug if the custom url is set to yes 70 | * 71 | * @param string $termlink The term link. 72 | * @param WP_Term $term The term object. 73 | * @return string 74 | */ 75 | public static function pre_term_link( $termlink, $term ) { 76 | if ( Taxonomy::SLUG !== $term->taxonomy ) { 77 | return $termlink; 78 | } 79 | 80 | $custom_url = get_term_meta( $term->term_id, Url_Meta::get_key(), true ); 81 | if ( 'yes' === $custom_url ) { 82 | $termlink = $term->slug; 83 | } 84 | 85 | return $termlink; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /tests/unit-tests/test-rest-theme-colors.php: -------------------------------------------------------------------------------- 1 | 'color_primary', 20 | 'color' => '#000000', 21 | ], 22 | [ 23 | 'name' => 'color_secondary', 24 | 'color' => '#0000FF', 25 | ], 26 | [ 27 | 'name' => 'color_secondary', 28 | 'color' => '#00c', 29 | ], 30 | ]; 31 | } 32 | 33 | /** 34 | * Test editing without permissions 35 | */ 36 | public function test_unauthorized() { 37 | wp_set_current_user( 0 ); 38 | $response = $this->dispatch_request_to_edit_termmeta( Theme_Colors::get_key(), $this->get_sample_valid_input() ); 39 | $this->assertSame( 401, $response->get_status() ); 40 | 41 | wp_set_current_user( $this->secondary_user_id->ID ); 42 | $response = $this->dispatch_request_to_edit_termmeta( Theme_Colors::get_key(), $this->get_sample_valid_input() ); 43 | $this->assertSame( 403, $response->get_status() ); 44 | } 45 | 46 | /** 47 | * Test setting a valid value 48 | */ 49 | public function test_valid_input() { 50 | wp_set_current_user( $this->user_id->ID ); 51 | $response = $this->dispatch_request_to_edit_termmeta( Theme_Colors::get_key(), $this->get_sample_valid_input() ); 52 | $data = $response->get_data(); 53 | 54 | $this->assertSame( 200, $response->get_status() ); 55 | $this->assertSame( $this->get_sample_valid_input(), $data['meta'][ Theme_Colors::get_key() ] ); 56 | } 57 | 58 | /** 59 | * Test setting an invalid value 60 | */ 61 | public function test_invalid_input() { 62 | wp_set_current_user( $this->user_id->ID ); 63 | $invalid = [ 64 | [ 65 | 'name' => 'color_primary', 66 | 'color' => '#000000', 67 | ], 68 | [ 69 | 'name' => 'color_secondary', 70 | 'color' => '#0000FF', 71 | ], 72 | [ 73 | 'name' => 'color_secondary', 74 | 'color' => '#00ZZZZ', 75 | ], 76 | ]; 77 | $response = $this->dispatch_request_to_edit_termmeta( Theme_Colors::get_key(), $invalid ); 78 | $data = $response->get_data(); 79 | $this->assertSame( 400, $response->get_status() ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "newspack-multibranded-site", 3 | "version": "2.2.0", 4 | "description": "A plugin to allow your site to host multiple brands", 5 | "license": "GPL-3.0", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/Automattic/newspack-multibranded-site.git" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/Automattic/newspack-multibranded-site/issues" 12 | }, 13 | "homepage": "https://github.com/Automattic/newspack-multibranded-site#readme", 14 | "scripts": { 15 | "cm": "git-cz", 16 | "semantic-release": "newspack-scripts release --files=newspack-multibranded-site.php", 17 | "clean": "rm -rf dist", 18 | "build": "npm run clean && newspack-scripts wp-scripts build", 19 | "start": "npm ci && npm run watch", 20 | "watch": "npm run clean && newspack-scripts wp-scripts start", 21 | "test": "echo 'No JS unit tests in this repository.'", 22 | "lint": "npm run lint:scss && npm run lint:js", 23 | "lint:js": "newspack-scripts wp-scripts lint-js '**/{src,includes}/**/*.{js,jsx,ts,tsx}'", 24 | "lint:js:staged": "newspack-scripts wp-scripts lint-js --ext .js,.jsx,.ts,.tsx", 25 | "fix:js": "newspack-scripts wp-scripts lint-js --fix '**/{src,includes}/**/*.{js,jsx,ts,tsx}'", 26 | "format:js": "newspack-scripts wp-scripts format '**/{src,includes}/**/*.{js,jsx,ts,tsx}'", 27 | "lint:php": "./vendor/bin/phpcs", 28 | "lint:php:staged": "./vendor/bin/phpcs --filter=GitStaged", 29 | "fix:php": "./vendor/bin/phpcbf", 30 | "lint:scss": "newspack-scripts wp-scripts lint-style '**/{src,includes}/**/*.scss' --customSyntax postcss-scss", 31 | "lint:scss:staged": "newspack-scripts wp-scripts lint-style --customSyntax postcss-scss", 32 | "format:scss": "newspack-scripts wp-scripts lint-style '**/{src,includes}/**/*.scss' --customSyntax postcss-scss --fix", 33 | "release": "npm run build && npm run semantic-release", 34 | "release:archive": "rm -rf release && mkdir -p release && rsync -r . ./release/newspack-multibranded-site --exclude-from='./.distignore' && cd release && zip -r newspack-multibranded-site.zip newspack-multibranded-site" 35 | }, 36 | "browserslist": [ 37 | "extends @wordpress/browserslist-config" 38 | ], 39 | "lint-staged": { 40 | "*.{js,jsx,ts,tsx}": "npm run lint:js:staged", 41 | "*.scss": "npm run lint:scss:staged", 42 | "*.php": "npm run lint:php:staged" 43 | }, 44 | "dependencies": { 45 | "react": "^18.3.0", 46 | "react-dom": "^18.3.0", 47 | "react-router-dom": "^6.26.1" 48 | }, 49 | "devDependencies": { 50 | "newspack-components": "^4.2.0", 51 | "newspack-scripts": "^5.8.0" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /includes/admin/class-post-primary-brand.php: -------------------------------------------------------------------------------- 1 | base || ! in_array( $screen->post_type, Taxonomy::get_post_types(), true ) ) { 35 | return; 36 | } 37 | 38 | wp_enqueue_script( 39 | 'newspack-post-primary-brand', 40 | plugins_url( '../../dist/postPrimaryBrand.js', __FILE__ ), 41 | array( 'wp-api-fetch', 'wp-components', 'wp-element', 'wp-i18n', 'wp-plugins', 'wp-url' ), 42 | filemtime( NEWSPACK_MULTIBRANDED_SITE_PLUGIN_DIR . '/dist/postPrimaryBrand.js' ), 43 | true 44 | ); 45 | 46 | wp_enqueue_style( 47 | 'newspack-post-primary-brand', 48 | plugins_url( '../../dist/postPrimaryBrand.css', __FILE__ ), 49 | [], 50 | filemtime( NEWSPACK_MULTIBRANDED_SITE_PLUGIN_DIR . '/dist/postPrimaryBrand.js' ) 51 | ); 52 | 53 | $page_slug = class_exists( 'Newspack\Newspack' ) ? 'newspack-settings#/additional-brands' : Admin::MULTI_BRANDED_PAGE_SLUG; 54 | wp_localize_script( 55 | 'newspack-post-primary-brand', 56 | 'newspackPostPrimaryBrandVars', 57 | array( 58 | 'adminURL' => admin_url( 'admin.php?page=' . $page_slug ), 59 | 'taxonomySlug' => Taxonomy::SLUG, 60 | 'metaKey' => Taxonomy::PRIMARY_META_KEY, 61 | 'postTypesWithPrimaryBrand' => Meta::get_post_types(), 62 | ) 63 | ); 64 | } 65 | 66 | /** 67 | * Removes Brands from the list of taxonomies for which Yoast will add a "primary term" selector. 68 | * 69 | * @param \WP_Taxonomy[] $taxonomies List of taxonomies. 70 | * @return \WP_Taxonomy[] 71 | */ 72 | public static function remove_yoast_primary_term( $taxonomies ) { 73 | if ( ! empty( $taxonomies[ Taxonomy::SLUG ] ) ) { 74 | unset( $taxonomies[ Taxonomy::SLUG ] ); 75 | } 76 | return $taxonomies; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/admin/views/brands/BrandsList.js: -------------------------------------------------------------------------------- 1 | import { NavLink, useNavigate } from 'react-router-dom'; 2 | 3 | import { useState, Fragment } from '@wordpress/element'; 4 | import { MenuItem } from '@wordpress/components'; 5 | import { moreVertical } from '@wordpress/icons'; 6 | import { ESCAPE } from '@wordpress/keycodes'; 7 | import { __ } from '@wordpress/i18n'; 8 | 9 | import { Card, ActionCard, Button, Popover, withWizardScreen } from 'newspack-components'; 10 | 11 | const AddNewBrandLink = () => ( 12 | 13 | { __( 'Add New Brand', 'newspack' ) } 14 | 15 | ); 16 | 17 | const BrandActionCard = ( { brand, deleteBrand } ) => { 18 | const [ popoverVisibility, setPopoverVisibility ] = useState( false ); 19 | const onFocusOutside = () => setPopoverVisibility( false ); 20 | const navigate = useNavigate(); 21 | 22 | return ( 23 | 28 | setPopoverVisibility( ! popoverVisibility ) } 30 | label={ __( 'More options', 'newspack' ) } 31 | icon={ moreVertical } 32 | className={ popoverVisibility && 'popover-active' } 33 | /> 34 | { popoverVisibility && ( 35 | ESCAPE === event.keyCode && onFocusOutside } 38 | onFocusOutside={ onFocusOutside } 39 | > 40 | onFocusOutside() } className="screen-reader-text"> 41 | { __( 'Close Popover', 'newspack' ) } 42 | 43 | navigate( `/brands/${ brand.id }` ) } className="newspack-button"> 44 | { __( 'Edit', 'newspack' ) } 45 | 46 | deleteBrand( brand ) } className="newspack-button"> 47 | { __( 'Delete', 'newspack' ) } 48 | 49 | 50 | ) } 51 | > 52 | } 53 | /> 54 | ); 55 | }; 56 | 57 | const BrandsList = ( { brands, deleteBrand } ) => { 58 | return brands.length ? ( 59 | 60 | 61 | { __( 'Site brands', 'newspack' ) } 62 | 63 | 64 | { brands.map( brand => ( 65 | 66 | ) ) } 67 | 68 | ) : ( 69 | 70 | 71 | { __( 'You have no saved brands.', 'newspack' ) } 72 | 73 | 74 | { __( 'Create brands to enhance your readers experience.', 'newspack' ) } 75 | 76 | ); 77 | }; 78 | 79 | export default withWizardScreen( BrandsList ); 80 | -------------------------------------------------------------------------------- /includes/admin/class-cat-primary-brand.php: -------------------------------------------------------------------------------- 1 | 'brand', 39 | 'hide_empty' => false, 40 | ] 41 | ); 42 | ?> 43 | 44 | 45 | 46 | 47 | 48 | 49 | term_id, \get_term_meta( $term->term_id, Taxonomy::PRIMARY_META_KEY, true ) ); ?>> 50 | name ); ?> 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | factory->term->create_and_get( [ 'taxonomy' => Taxonomy::SLUG ] ); 24 | $term_with_theme_colors = $this->factory->term->create_and_get( [ 'taxonomy' => Taxonomy::SLUG ] ); 25 | add_term_meta( 26 | $term_with_theme_colors->term_id, 27 | Theme_Colors_Meta::get_key(), 28 | [ 29 | [ 30 | 'name' => 'primary_color', 31 | 'color' => '#000000', 32 | ], 33 | ] 34 | ); 35 | 36 | $this->go_to( get_term_link( $term_without_theme_colors->term_id ) ); 37 | $this->assertSame( false, get_theme_mod( 'primary_color' ) ); 38 | 39 | $this->go_to( get_term_link( $term_with_theme_colors->term_id ) ); 40 | $this->assertSame( '#000000', get_theme_mod( 'primary_color' ) ); 41 | } 42 | 43 | /** 44 | * Tests has theme colors 45 | */ 46 | public function test_has_theme_colors() { 47 | $term_without_theme_colors = $this->factory->term->create_and_get( [ 'taxonomy' => Taxonomy::SLUG ] ); 48 | $term_with_theme_colors = $this->factory->term->create_and_get( [ 'taxonomy' => Taxonomy::SLUG ] ); 49 | add_term_meta( 50 | $term_with_theme_colors->term_id, 51 | Theme_Colors_Meta::get_key(), 52 | [ 53 | [ 54 | 'name' => 'primary_color', 55 | 'color' => '#000000', 56 | ], 57 | ] 58 | ); 59 | 60 | $this->go_to( get_term_link( $term_without_theme_colors->term_id ) ); 61 | $this->assertFalse( Theme_Colors_Customization::current_brand_has_custom_colors() ); 62 | $this->assertFalse( Theme_Colors_Customization::current_brand_has_custom_colors( [ 'primary_color' ] ) ); 63 | $this->assertFalse( Theme_Colors_Customization::current_brand_has_custom_colors( [ 'primary_color', 'secondary_color' ] ) ); 64 | $this->assertFalse( Theme_Colors_Customization::current_brand_has_custom_colors( [ 'secondary_color' ] ) ); 65 | 66 | $this->go_to( get_term_link( $term_with_theme_colors->term_id ) ); 67 | $this->assertTrue( Theme_Colors_Customization::current_brand_has_custom_colors() ); 68 | $this->assertTrue( Theme_Colors_Customization::current_brand_has_custom_colors( [ 'primary_color' ] ) ); 69 | $this->assertTrue( Theme_Colors_Customization::current_brand_has_custom_colors( [ 'primary_color', 'secondary_color' ] ) ); 70 | $this->assertFalse( Theme_Colors_Customization::current_brand_has_custom_colors( [ 'secondary_color' ] ) ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /includes/admin/class-user-primary-brand.php: -------------------------------------------------------------------------------- 1 | 'brand', 56 | 'hide_empty' => false, 57 | ] 58 | ); 59 | ?> 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | term_id, \get_user_meta( $user->ID, Meta::get_key(), true ) ); ?>> 76 | name ); ?> 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | post = $this->factory->post->create_and_get(); 32 | $this->post_by_author = $this->factory->post->create_and_get( [ 'post_author' => $this->author->ID ] ); 33 | } 34 | 35 | /** 36 | * Test editing without permissions 37 | */ 38 | public function test_unauthorized() { 39 | wp_set_current_user( 0 ); 40 | $response = $this->dispatch_request_to_edit_postmeta( $this->post->ID, Taxonomy::PRIMARY_META_KEY, 123 ); 41 | $this->assertSame( 401, $response->get_status() ); 42 | 43 | wp_set_current_user( $this->author->ID ); 44 | $response = $this->dispatch_request_to_edit_postmeta( $this->post->ID, Taxonomy::PRIMARY_META_KEY, 123 ); 45 | $this->assertSame( 403, $response->get_status() ); 46 | } 47 | 48 | /** 49 | * Test with permissions 50 | */ 51 | public function test_authorized() { 52 | wp_set_current_user( $this->secondary_user_id->ID ); 53 | $response = $this->dispatch_request_to_edit_postmeta( $this->post_by_author->ID, Taxonomy::PRIMARY_META_KEY, 123 ); 54 | $this->assertSame( 200, $response->get_status() ); 55 | 56 | wp_set_current_user( $this->author->ID ); 57 | $response = $this->dispatch_request_to_edit_postmeta( $this->post_by_author->ID, Taxonomy::PRIMARY_META_KEY, 123 ); 58 | $this->assertSame( 200, $response->get_status(), 'Authors have permission to edit their own posts' ); 59 | 60 | wp_set_current_user( $this->secondary_user_id->ID ); 61 | $response = $this->dispatch_request_to_edit_postmeta( $this->post->ID, Taxonomy::PRIMARY_META_KEY, 123 ); 62 | $this->assertSame( 200, $response->get_status(), 'Editors have permission to edit other posts' ); 63 | $response = $this->dispatch_request_to_edit_postmeta( $this->post_by_author->ID, Taxonomy::PRIMARY_META_KEY, 123 ); 64 | $this->assertSame( 200, $response->get_status(), 'Editors have permission to edit other posts' ); 65 | } 66 | 67 | /** 68 | * Test setting a valid value 69 | */ 70 | public function test_valid_input() { 71 | wp_set_current_user( $this->user_id->ID ); 72 | $response = $this->dispatch_request_to_edit_postmeta( $this->post->ID, Taxonomy::PRIMARY_META_KEY, 123 ); 73 | $data = $response->get_data(); 74 | 75 | $this->assertSame( 200, $response->get_status() ); 76 | $this->assertSame( 123, $data['meta'][ Taxonomy::PRIMARY_META_KEY ] ); 77 | } 78 | 79 | /** 80 | * Test setting an invalid value 81 | */ 82 | public function test_invalid_input() { 83 | wp_set_current_user( $this->user_id->ID ); 84 | $response = $this->dispatch_request_to_edit_postmeta( $this->post->ID, Taxonomy::PRIMARY_META_KEY, 'invalid' ); 85 | $data = $response->get_data(); 86 | $this->assertSame( 400, $response->get_status() ); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /includes/class-meta.php: -------------------------------------------------------------------------------- 1 | static::get_description(), 38 | 'single' => true, 39 | 'show_in_rest' => [ 40 | 'schema' => static::get_schema(), 41 | ], 42 | 'type' => $type, 43 | 'auth_callback' => [ get_called_class(), 'auth_callback' ], 44 | ]; 45 | 46 | if ( 'post' === static::$type ) { 47 | $post_types = static::get_post_types(); 48 | foreach ( $post_types as $post_type ) { 49 | $params['object_subtype'] = $post_type; 50 | register_meta( 51 | 'post', 52 | static::get_key(), 53 | $params 54 | ); 55 | } 56 | return; 57 | } 58 | 59 | if ( 'term' === static::$type ) { 60 | $params['object_subtype'] = static::get_taxonomy(); 61 | } 62 | register_meta( 63 | static::$type, 64 | static::get_key(), 65 | $params 66 | ); 67 | } 68 | 69 | /** 70 | * Get the taxonomy to register the meta to, if meta type is term 71 | * 72 | * @return string 73 | */ 74 | public static function get_taxonomy() { 75 | return Taxonomy::SLUG; 76 | } 77 | 78 | /** 79 | * Get the post types to register the meta to, if meta type is post 80 | * 81 | * @return array 82 | */ 83 | public static function get_post_types() { 84 | return []; 85 | } 86 | 87 | /** 88 | * Returns whether the current user can edit the meta 89 | * 90 | * @param bool $allowed Whether the user can add the object meta. Default false. 91 | * @param string $meta_key The meta key. 92 | * @param int $object_id Object ID. 93 | * @return bool 94 | */ 95 | public static function auth_callback( $allowed, $meta_key, $object_id ) { 96 | return current_user_can( static::get_capability(), $object_id ); 97 | } 98 | 99 | /** 100 | * Returns the capability needed to edit the meta 101 | * 102 | * @return string 103 | */ 104 | public static function get_capability() { 105 | if ( 'post' === static::$type ) { 106 | return 'edit_post'; // singular, to check meta cap against the post id. 107 | } 108 | if ( 'term' === static::$type ) { 109 | $tax_object = get_taxonomy( static::get_taxonomy() ); 110 | return $tax_object->cap->manage_terms; 111 | } 112 | if ( 'user' === static::$type ) { 113 | return 'edit_users'; 114 | } 115 | 116 | // default to manage_options, but this should never happen. 117 | return 'manage_options'; 118 | } 119 | 120 | /** 121 | * Gets the meta key 122 | * 123 | * @return string 124 | */ 125 | abstract public static function get_key(); 126 | 127 | /** 128 | * Gets the meta description 129 | * 130 | * @return string 131 | */ 132 | abstract public static function get_description(); 133 | 134 | /** 135 | * Gets the meta schema 136 | * 137 | * @return array 138 | */ 139 | abstract public static function get_schema(); 140 | } 141 | -------------------------------------------------------------------------------- /includes/customizations/class-theme-colors.php: -------------------------------------------------------------------------------- 1 | term_id, Theme_Colors_Meta::get_key(), true ); 72 | if ( ! empty( $custom_colors ) ) { 73 | if ( empty( $color_names ) ) { 74 | return true; 75 | } 76 | foreach ( $custom_colors as $custom_color ) { 77 | if ( in_array( $custom_color['name'], $color_names, true ) ) { 78 | return true; 79 | } 80 | } 81 | } 82 | return false; 83 | } 84 | 85 | /** 86 | * Filters the theme color 87 | * 88 | * @param string $value The theme mod value. 89 | * @return string 90 | */ 91 | public static function filter_theme_color( $value ) { 92 | $brand = Taxonomy::get_current(); 93 | if ( ! $brand ) { 94 | return $value; 95 | } 96 | $theme_mod_name = str_replace( 'theme_mod_', '', current_filter() ); 97 | $custom_colors = get_term_meta( $brand->term_id, Theme_Colors_Meta::get_key(), true ); 98 | 99 | if ( ! empty( $custom_colors ) ) { 100 | foreach ( $custom_colors as $custom_color ) { 101 | if ( $custom_color['name'] === $theme_mod_name ) { 102 | return $custom_color['color']; 103 | } 104 | } 105 | } 106 | 107 | return $value; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/post-primary-brand/index.js: -------------------------------------------------------------------------------- 1 | /* global newspackPostPrimaryBrandVars */ 2 | 3 | import { __ } from '@wordpress/i18n'; 4 | import { Button, Flex, FlexItem, SelectControl } from '@wordpress/components'; 5 | import { useDispatch, useSelect } from '@wordpress/data'; 6 | import { store as coreStore } from '@wordpress/core-data'; 7 | 8 | import './index.scss'; 9 | 10 | /** 11 | * Module Constants 12 | */ 13 | const DEFAULT_QUERY = { 14 | per_page: -1, 15 | orderby: 'name', 16 | order: 'asc', 17 | _fields: 'id,name,parent', 18 | context: 'view', 19 | }; 20 | 21 | const EMPTY_ARRAY = []; 22 | 23 | const ZERO = 0; 24 | 25 | const ADMIN_URL = newspackPostPrimaryBrandVars.adminURL; 26 | 27 | const TAXONOMY_SLUG = newspackPostPrimaryBrandVars.taxonomySlug; 28 | 29 | const META_KEY = newspackPostPrimaryBrandVars.metaKey; 30 | 31 | const SHOW_PRIMARY_BRAND_FOR = newspackPostPrimaryBrandVars.postTypesWithPrimaryBrand; 32 | 33 | /** 34 | * Adds a primary brand selector to the post editor. 35 | */ 36 | const NewspackPostPrimaryBrand = ( { slug } ) => { 37 | const { editPost } = useDispatch( 'core/editor' ); 38 | 39 | const { terms, availableTerms, primaryBrand, postType } = useSelect( 40 | select => { 41 | const { getEditedPostAttribute, getCurrentPostType } = select( 'core/editor' ); 42 | const { getTaxonomy, getEntityRecords } = select( coreStore ); 43 | const _taxonomy = getTaxonomy( slug ); 44 | const _meta = getEditedPostAttribute( 'meta' ); 45 | const _postType = getCurrentPostType(); 46 | 47 | return { 48 | terms: _taxonomy ? getEditedPostAttribute( _taxonomy.rest_base ) : EMPTY_ARRAY, 49 | availableTerms: getEntityRecords( 'taxonomy', slug, DEFAULT_QUERY ) || EMPTY_ARRAY, 50 | primaryBrand: _meta[ META_KEY ], 51 | postType: _postType, 52 | }; 53 | }, 54 | [ slug ] 55 | ); 56 | 57 | const getTermSelectOptionFromId = id => { 58 | const term = availableTerms.find( t => t.id === id ); 59 | return term ? { value: term.id, label: term.name } : null; 60 | }; 61 | 62 | const onChangePrimaryBrand = termId => { 63 | editPost( { meta: { [ META_KEY ]: termId } } ); 64 | }; 65 | 66 | const shouldDisplayPrimaryBrand = SHOW_PRIMARY_BRAND_FOR.includes( postType ); 67 | 68 | return ( 69 | 70 | { shouldDisplayPrimaryBrand && ( 71 | 72 | { terms.length > 1 && ( 73 | getTermSelectOptionFromId( term ) ).filter( term => term ), 82 | ] } 83 | onChange={ onChangePrimaryBrand } 84 | /> 85 | ) } 86 | 87 | ) } 88 | 89 | 90 | 91 | { __( 'Manage Brands', 'newspack-multibranded-site' ) } 92 | 93 | 94 | 95 | ); 96 | }; 97 | 98 | function customizeSelector( OriginalComponent ) { 99 | return function ( props ) { 100 | if ( props.slug === TAXONOMY_SLUG ) { 101 | return ( 102 | 103 | 104 | 105 | 106 | ); 107 | } 108 | return ; 109 | }; 110 | } 111 | 112 | wp.hooks.addFilter( 'editor.PostTaxonomyType', 'newspack/multibranded-site/brand-selector-filter', customizeSelector ); 113 | -------------------------------------------------------------------------------- /phpcsSniffs/Sniffs/NewspackNewslettersMethodsSniff.php: -------------------------------------------------------------------------------- 1 | methods and check if they are called from the allowed classes. 87 | * They are also allowed to be called from within the service-providers directory. 88 | * 89 | * @param PHP_CodeSniffer_File $phpcs_file The file where the token was found. 90 | * @param int $stack_ptr The position in the stack where the token was found. 91 | */ 92 | public function process( PHP_CodeSniffer_File $phpcs_file, $stack_ptr ) { 93 | 94 | $tokens = $phpcs_file->getTokens(); 95 | $token = $tokens[ $stack_ptr ]; 96 | 97 | if ( in_array( $token['content'], $this->methods, true ) ) { 98 | $operator = $tokens[ $stack_ptr - 1 ]; 99 | 100 | if ( $operator['type'] === 'T_DOUBLE_COLON' ) { 101 | 102 | $class_name = $tokens[ $stack_ptr - 2 ]['content']; 103 | if ( in_array( $class_name, $this->static_classes, true ) ) { 104 | 105 | $method_name = $class_name . '::' . $token['content'] . '()'; 106 | 107 | $phpcs_file->addError( 108 | sprintf( self::ERROR_MESSAGE, $method_name ), 109 | $stack_ptr, 110 | self::ERROR_CODE 111 | ); 112 | } 113 | } elseif ( $operator['type'] === 'T_OBJECT_OPERATOR' ) { 114 | 115 | $method_name = $token['content']; 116 | 117 | $phpcs_file->addWarning( 118 | sprintf( self::WARNING_MESSAGE, $method_name ), 119 | $stack_ptr, 120 | self::WARNING_CODE 121 | ); 122 | } 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /tests/unit-tests/test-customization-should-display-prompt.php: -------------------------------------------------------------------------------- 1 | assertTrue( $should_display ); 22 | 23 | $should_display = Popups_Should_Display_Prompt::filter_should_display( true, 'invalid' ); 24 | $this->assertTrue( $should_display ); 25 | } 26 | 27 | /** 28 | * Test all scenarios 29 | */ 30 | public function test_no_brand() { 31 | $prompt_without_brands = $this->factory->post->create_and_get(); 32 | $prompt_with_one_brand = $this->factory->post->create_and_get(); 33 | $prompt_with_two_brands = $this->factory->post->create_and_get(); 34 | 35 | $brand1 = $this->factory->term->create_and_get( array( 'taxonomy' => Taxonomy::SLUG ) ); 36 | $brand2 = $this->factory->term->create_and_get( array( 'taxonomy' => Taxonomy::SLUG ) ); 37 | $brand3 = $this->factory->term->create_and_get( array( 'taxonomy' => Taxonomy::SLUG ) ); 38 | 39 | wp_set_post_terms( $prompt_with_one_brand->ID, $brand1->term_id, Taxonomy::SLUG ); 40 | wp_set_post_terms( $prompt_with_two_brands->ID, [ $brand1->term_id, $brand2->term_id ], Taxonomy::SLUG ); 41 | 42 | // No current brand. 43 | $this->go_to( '/' ); 44 | $should_display = Popups_Should_Display_Prompt::filter_should_display( true, [ 'id' => $prompt_without_brands->ID ] ); 45 | $this->assertEquals( true, $should_display ); 46 | 47 | $should_display = Popups_Should_Display_Prompt::filter_should_display( true, [ 'id' => $prompt_with_one_brand->ID ] ); 48 | $this->assertEquals( false, $should_display ); 49 | 50 | $should_display = Popups_Should_Display_Prompt::filter_should_display( true, [ 'id' => $prompt_with_two_brands->ID ] ); 51 | $this->assertEquals( false, $should_display ); 52 | 53 | // Brand 1. 54 | $this->go_to( get_term_link( $brand1 ) ); 55 | $should_display = Popups_Should_Display_Prompt::filter_should_display( true, [ 'id' => $prompt_without_brands->ID ] ); 56 | $this->assertEquals( true, $should_display ); 57 | 58 | $should_display = Popups_Should_Display_Prompt::filter_should_display( true, [ 'id' => $prompt_with_one_brand->ID ] ); 59 | $this->assertEquals( true, $should_display ); 60 | 61 | $should_display = Popups_Should_Display_Prompt::filter_should_display( true, [ 'id' => $prompt_with_two_brands->ID ] ); 62 | $this->assertEquals( true, $should_display ); 63 | 64 | // Brand 2. 65 | $this->go_to( get_term_link( $brand2 ) ); 66 | $should_display = Popups_Should_Display_Prompt::filter_should_display( true, [ 'id' => $prompt_without_brands->ID ] ); 67 | $this->assertEquals( true, $should_display ); 68 | 69 | $should_display = Popups_Should_Display_Prompt::filter_should_display( true, [ 'id' => $prompt_with_one_brand->ID ] ); 70 | $this->assertEquals( false, $should_display ); 71 | 72 | $should_display = Popups_Should_Display_Prompt::filter_should_display( true, [ 'id' => $prompt_with_two_brands->ID ] ); 73 | $this->assertEquals( true, $should_display ); 74 | 75 | // If the initial value is false, the return should always be false. 76 | $should_display = Popups_Should_Display_Prompt::filter_should_display( false, [ 'id' => $prompt_with_two_brands->ID ] ); 77 | $this->assertEquals( false, $should_display ); 78 | 79 | // Brand 3. 80 | $this->go_to( get_term_link( $brand3 ) ); 81 | $should_display = Popups_Should_Display_Prompt::filter_should_display( true, [ 'id' => $prompt_without_brands->ID ] ); 82 | $this->assertEquals( true, $should_display ); 83 | 84 | $should_display = Popups_Should_Display_Prompt::filter_should_display( true, [ 'id' => $prompt_with_one_brand->ID ] ); 85 | $this->assertEquals( false, $should_display ); 86 | 87 | $should_display = Popups_Should_Display_Prompt::filter_should_display( true, [ 'id' => $prompt_with_two_brands->ID ] ); 88 | $this->assertEquals( false, $should_display ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /tests/class-newspack-multibranded-rest-testcase.php: -------------------------------------------------------------------------------- 1 | server = $wp_rest_server; 74 | 75 | do_action( 'rest_api_init' ); 76 | 77 | $this->user_id = $this->factory->user->create_and_get( 78 | array( 79 | 'role' => 'administrator', 80 | ) 81 | ); 82 | 83 | $this->secondary_user_id = $this->factory->user->create_and_get( 84 | array( 85 | 'role' => 'editor', 86 | ) 87 | ); 88 | 89 | $this->author = $this->factory->user->create_and_get( 90 | array( 91 | 'role' => 'author', 92 | ) 93 | ); 94 | 95 | $this->subscriber = $this->factory->user->create_and_get( 96 | array( 97 | 'role' => 'subscriber', 98 | ) 99 | ); 100 | 101 | $this->term1 = $this->factory->term->create_and_get( array( 'taxonomy' => Taxonomy::SLUG ) ); 102 | } 103 | 104 | /** 105 | * Dispatches a POST request to edit the term metadata 106 | * 107 | * @param string $key The new meta key. 108 | * @param string $value The new meta value. 109 | * @return WP_REST_Response 110 | */ 111 | protected function dispatch_request_to_edit_termmeta( $key, $value ) { 112 | $endpoint = '/wp/v2/' . Taxonomy::SLUG . '/' . $this->term1->term_id; 113 | 114 | $request = new WP_REST_Request( 115 | 'POST', 116 | $endpoint 117 | ); 118 | 119 | $request->set_header( 'content-type', 'application/json' ); 120 | 121 | $request->set_body( 122 | wp_json_encode( 123 | [ 124 | 'meta' => [ 125 | $key => $value, 126 | ], 127 | ] 128 | ) 129 | ); 130 | 131 | $response = $this->server->dispatch( $request ); 132 | 133 | return $response; 134 | } 135 | 136 | /** 137 | * Dispatches a POST request to edit a post metadata 138 | * 139 | * @param int $post_id The post ID. 140 | * @param string $key The meta key. 141 | * @param string $value The meta value. 142 | * @return WP_REST_Response 143 | */ 144 | protected function dispatch_request_to_edit_postmeta( $post_id, $key, $value ) { 145 | $endpoint = '/wp/v2/posts/' . $post_id; 146 | 147 | $request = new WP_REST_Request( 148 | 'POST', 149 | $endpoint 150 | ); 151 | 152 | $request->set_header( 'content-type', 'application/json' ); 153 | 154 | $request->set_body( 155 | wp_json_encode( 156 | [ 157 | 'meta' => [ 158 | $key => $value, 159 | ], 160 | ] 161 | ) 162 | ); 163 | 164 | $response = $this->server->dispatch( $request ); 165 | 166 | return $response; 167 | } 168 | 169 | /** 170 | * Dispatches a POST request to edit the site option 171 | * 172 | * @param string $option_name The option name. 173 | * @param string $value The option value. 174 | * @return WP_REST_Response 175 | */ 176 | protected function dispatch_request_to_edit_option( $option_name, $value ) { 177 | $endpoint = '/wp/v2/settings'; 178 | 179 | $request = new WP_REST_Request( 180 | 'POST', 181 | $endpoint 182 | ); 183 | 184 | $request->set_header( 'content-type', 'application/json' ); 185 | 186 | $request->set_body( 187 | wp_json_encode( 188 | [ 189 | $option_name => $value, 190 | ] 191 | ) 192 | ); 193 | 194 | $response = $this->server->dispatch( $request ); 195 | 196 | return $response; 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/admin/views/brands/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Routes, Route, useNavigate } from 'react-router-dom'; 3 | import { withWizard } from 'newspack-components'; 4 | 5 | import { addQueryArgs } from '@wordpress/url'; 6 | import { useState, useEffect } from '@wordpress/element'; 7 | import { __ } from '@wordpress/i18n'; 8 | 9 | import BrandsList from './BrandsList'; 10 | import Brand from './Brand'; 11 | 12 | const Brands = ( { setError, wizardApiFetch } ) => { 13 | const [ brands, setBrands ] = useState( [] ); 14 | const navigate = useNavigate(); 15 | 16 | const headerText = __( 'Brands', 'newspack' ); 17 | const subHeaderText = __( 'Configure brands settings', 'newspack' ); 18 | const wizardScreenProps = { 19 | headerText, 20 | subHeaderText, 21 | }; 22 | 23 | /** 24 | * Fetching brands data. 25 | */ 26 | const fetchBrands = () => { 27 | wizardApiFetch( { 28 | path: addQueryArgs( '/wp/v2/brand', { per_page: 100 } ), 29 | } ) 30 | .then( response => 31 | setBrands( 32 | response.map( brand => ( { 33 | ...brand, 34 | meta: { 35 | ...brand.meta, 36 | _theme_colors: 0 === brand.meta._theme_colors?.length ? null : brand.meta._theme_colors, 37 | _menus: 0 === brand.meta._menus?.length ? null : brand.meta._menus, 38 | }, 39 | } ) ) 40 | ) 41 | ) 42 | .catch( error => setError( error ) ); 43 | }; 44 | 45 | const saveBrand = ( brandId, brand ) => { 46 | wizardApiFetch( { 47 | path: brandId ? `/wp/v2/brand/${ brandId }` : '/wp/v2/brand', 48 | method: 'POST', 49 | data: { 50 | ...brand, 51 | meta: { 52 | ...brand.meta, 53 | ...( brand.meta._logo && { _logo: brand.meta._logo.id } ), 54 | }, 55 | }, 56 | quiet: true, 57 | } ) 58 | .then( result => 59 | setBrands( brandsList => { 60 | // The result from the API call doesn't contain the logo details. 61 | const newBrand = { id: result.id, ...brand }; 62 | if ( brandId ) { 63 | const brandIndex = brandsList.findIndex( _brand => brandId === _brand.id ); 64 | if ( brandIndex > -1 ) { 65 | return brandsList.map( _brand => ( brandId === _brand.id ? newBrand : _brand ) ); 66 | } 67 | } 68 | 69 | return [ newBrand, ...brandsList ]; 70 | } ) 71 | ) 72 | .then( navigate( '/' ) ) 73 | .catch( setError ); 74 | }; 75 | 76 | const deleteBrand = brand => { 77 | // eslint-disable-next-line no-alert 78 | if ( confirm( __( 'Are you sure you want to delete this brand?', 'newspack' ) ) ) { 79 | return wizardApiFetch( { 80 | path: addQueryArgs( `/wp/v2/brand/${ brand.id }`, { force: true } ), 81 | method: 'DELETE', 82 | quiet: true, 83 | } ) 84 | .then( result => { 85 | if ( result.deleted ) { 86 | setBrands( oldBrands => oldBrands.filter( oldBrand => brand.id !== oldBrand.id ) ); 87 | } 88 | } ) 89 | .catch( e => { 90 | setError( e ); 91 | } ); 92 | } 93 | }; 94 | 95 | const fetchLogoAttachment = ( brandId, attachmentId ) => { 96 | if ( ! attachmentId ) { 97 | return; 98 | } 99 | wizardApiFetch( { 100 | path: `/wp/v2/media/${ attachmentId }`, 101 | method: 'GET', 102 | } ) 103 | .then( attachment => 104 | setBrands( brandsList => { 105 | const brandIndex = brandsList.findIndex( _brand => brandId === _brand.id ); 106 | return brandIndex > -1 107 | ? brandsList.map( _brand => 108 | brandId === _brand.id 109 | ? { 110 | ..._brand, 111 | meta: { 112 | ..._brand.meta, 113 | _logo: { ...attachment, url: attachment.source_url }, 114 | }, 115 | } 116 | : _brand 117 | ) 118 | : brandsList; 119 | } ) 120 | ) 121 | .catch( setError ); 122 | }; 123 | 124 | useEffect( fetchBrands, [] ); 125 | 126 | return ( 127 | 128 | } /> 129 | } 132 | /> 133 | 144 | } 145 | /> 146 | 147 | ); 148 | }; 149 | 150 | export default withWizard( Brands ); 151 | -------------------------------------------------------------------------------- /includes/class-admin.php: -------------------------------------------------------------------------------- 1 | '; 76 | } 77 | 78 | /** 79 | * Callback for the load admin page hook. 80 | * 81 | * @return void 82 | */ 83 | public static function admin_init() { 84 | add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_scripts' ) ); 85 | add_filter( 'admin_body_class', array( __CLASS__, 'body_class' ) ); 86 | } 87 | 88 | /** 89 | * Add Newspack admin body class, necessary for wizard styling. 90 | * 91 | * @param string $classes Space-separated list of CSS classes. 92 | * @return string 93 | */ 94 | public static function body_class( $classes ) { 95 | $screen = get_current_screen(); 96 | 97 | $is_newspack_screen = ( 'toplevel_page_newspack-' === substr( $screen->base, 0, 23 ) ); 98 | if ( ! $screen || ! $is_newspack_screen ) { 99 | return $classes; 100 | } 101 | 102 | $classes .= ' admin_page_newspack-multi-branded-sites'; 103 | 104 | return $classes; 105 | } 106 | 107 | /** 108 | * Enqueue admin page assets. 109 | * 110 | * @param string $handler Page handler. 111 | * 112 | * @return void 113 | */ 114 | public static function enqueue_scripts( $handler ) { 115 | if ( false === strpos( $handler, self::MULTI_BRANDED_PAGE_SLUG ) ) { 116 | return; 117 | } 118 | 119 | \wp_register_script( 120 | self::MULTI_BRANDED_PAGE_SLUG, 121 | plugins_url( '../dist/admin.js', __FILE__ ), 122 | array( 'wp-components', 'wp-api-fetch' ), 123 | filemtime( NEWSPACK_MULTIBRANDED_SITE_PLUGIN_DIR . 'dist/admin.js' ), 124 | true 125 | ); 126 | 127 | $menus = array_map( 128 | function ( $menu ) { 129 | return array( 130 | 'value' => $menu->term_id, 131 | 'label' => $menu->name, 132 | ); 133 | }, 134 | wp_get_nav_menus() 135 | ); 136 | 137 | $aux_data = array( 138 | 'theme_colors' => Customizations\Theme_Colors::get_registered_theme_colors(), 139 | 'menu_locations' => get_registered_nav_menus(), 140 | 'menus' => $menus, 141 | 'site' => get_site_url(), 142 | ); 143 | 144 | wp_localize_script( 145 | self::MULTI_BRANDED_PAGE_SLUG, 146 | 'newspack_urls', 147 | array( 148 | 'dashboard' => esc_url( admin_url( 'admin.php?page=' . self::MULTI_BRANDED_PAGE_SLUG ) ), 149 | 'support' => esc_url( 'https://help.newspack.com/' ), 150 | ) 151 | ); 152 | wp_localize_script( self::MULTI_BRANDED_PAGE_SLUG, 'newspack_aux_data', $aux_data ); 153 | 154 | \wp_enqueue_script( self::MULTI_BRANDED_PAGE_SLUG ); 155 | 156 | \wp_register_style( 157 | self::MULTI_BRANDED_PAGE_SLUG, 158 | plugins_url( '../dist/admin.css', __FILE__ ), 159 | array( 'wp-components' ), 160 | filemtime( NEWSPACK_MULTIBRANDED_SITE_PLUGIN_DIR . 'dist/admin.css' ) 161 | ); 162 | \wp_style_add_data( self::MULTI_BRANDED_PAGE_SLUG, 'rtl', 'replace' ); 163 | \wp_enqueue_style( self::MULTI_BRANDED_PAGE_SLUG ); 164 | 165 | \wp_enqueue_style( // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion 166 | 'tachyons', 167 | 'https://unpkg.com/tachyons@4.12.0/css/tachyons.min.css' 168 | ); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /bin/install-wp-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ $# -lt 3 ]; then 4 | echo "usage: $0 [db-host] [wp-version] [skip-database-creation]" 5 | exit 1 6 | fi 7 | 8 | DB_NAME=$1 9 | DB_USER=$2 10 | DB_PASS=$3 11 | DB_HOST=${4-localhost} 12 | WP_VERSION=${5-latest} 13 | SKIP_DB_CREATE=${6-false} 14 | 15 | TMPDIR=${TMPDIR-/tmp} 16 | TMPDIR=$(echo $TMPDIR | sed -e "s/\/$//") 17 | WP_TESTS_DIR=${WP_TESTS_DIR-$TMPDIR/wordpress-tests-lib} 18 | WP_CORE_DIR=${WP_CORE_DIR-$TMPDIR/wordpress} 19 | 20 | download() { 21 | if [ `which curl` ]; then 22 | curl -s "$1" > "$2"; 23 | elif [ `which wget` ]; then 24 | wget -nv -O "$2" "$1" 25 | fi 26 | } 27 | 28 | if [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+\-(beta|RC)[0-9]+$ ]]; then 29 | WP_BRANCH=${WP_VERSION%\-*} 30 | WP_TESTS_TAG="branches/$WP_BRANCH" 31 | 32 | elif [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+$ ]]; then 33 | WP_TESTS_TAG="branches/$WP_VERSION" 34 | elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then 35 | if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then 36 | # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x 37 | WP_TESTS_TAG="tags/${WP_VERSION%??}" 38 | else 39 | WP_TESTS_TAG="tags/$WP_VERSION" 40 | fi 41 | elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then 42 | WP_TESTS_TAG="trunk" 43 | else 44 | # http serves a single offer, whereas https serves multiple. we only want one 45 | download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json 46 | grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json 47 | LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//') 48 | if [[ -z "$LATEST_VERSION" ]]; then 49 | echo "Latest WordPress version could not be found" 50 | exit 1 51 | fi 52 | WP_TESTS_TAG="tags/$LATEST_VERSION" 53 | fi 54 | set -ex 55 | 56 | install_wp() { 57 | 58 | if [ -d $WP_CORE_DIR ]; then 59 | return; 60 | fi 61 | 62 | mkdir -p $WP_CORE_DIR 63 | 64 | if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then 65 | mkdir -p $TMPDIR/wordpress-trunk 66 | rm -rf $TMPDIR/wordpress-trunk/* 67 | svn export --quiet https://core.svn.wordpress.org/trunk $TMPDIR/wordpress-trunk/wordpress 68 | mv $TMPDIR/wordpress-trunk/wordpress/* $WP_CORE_DIR 69 | else 70 | if [ $WP_VERSION == 'latest' ]; then 71 | local ARCHIVE_NAME='latest' 72 | elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+ ]]; then 73 | # https serves multiple offers, whereas http serves single. 74 | download https://api.wordpress.org/core/version-check/1.7/ $TMPDIR/wp-latest.json 75 | if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then 76 | # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x 77 | LATEST_VERSION=${WP_VERSION%??} 78 | else 79 | # otherwise, scan the releases and get the most up to date minor version of the major release 80 | local VERSION_ESCAPED=`echo $WP_VERSION | sed 's/\./\\\\./g'` 81 | LATEST_VERSION=$(grep -o '"version":"'$VERSION_ESCAPED'[^"]*' $TMPDIR/wp-latest.json | sed 's/"version":"//' | head -1) 82 | fi 83 | if [[ -z "$LATEST_VERSION" ]]; then 84 | local ARCHIVE_NAME="wordpress-$WP_VERSION" 85 | else 86 | local ARCHIVE_NAME="wordpress-$LATEST_VERSION" 87 | fi 88 | else 89 | local ARCHIVE_NAME="wordpress-$WP_VERSION" 90 | fi 91 | download https://wordpress.org/${ARCHIVE_NAME}.tar.gz $TMPDIR/wordpress.tar.gz 92 | tar --strip-components=1 -zxmf $TMPDIR/wordpress.tar.gz -C $WP_CORE_DIR 93 | fi 94 | 95 | download https://raw.githubusercontent.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php 96 | } 97 | 98 | install_test_suite() { 99 | # portable in-place argument for both GNU sed and Mac OSX sed 100 | if [[ $(uname -s) == 'Darwin' ]]; then 101 | local ioption='-i.bak' 102 | else 103 | local ioption='-i' 104 | fi 105 | 106 | # set up testing suite if it doesn't yet exist 107 | if [ ! -d $WP_TESTS_DIR ]; then 108 | # set up testing suite 109 | mkdir -p $WP_TESTS_DIR 110 | rm -rf $WP_TESTS_DIR/{includes,data} 111 | svn export --quiet --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes 112 | svn export --quiet --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data 113 | fi 114 | 115 | if [ ! -f wp-tests-config.php ]; then 116 | download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php 117 | # remove all forward slashes in the end 118 | WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::") 119 | sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php 120 | sed $ioption "s:__DIR__ . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php 121 | sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php 122 | sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php 123 | sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php 124 | sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php 125 | fi 126 | 127 | } 128 | 129 | recreate_db() { 130 | shopt -s nocasematch 131 | if [[ $1 =~ ^(y|yes)$ ]] 132 | then 133 | mysqladmin drop $DB_NAME -f --user="$DB_USER" --password="$DB_PASS"$EXTRA 134 | create_db 135 | echo "Recreated the database ($DB_NAME)." 136 | else 137 | echo "Leaving the existing database ($DB_NAME) in place." 138 | fi 139 | shopt -u nocasematch 140 | } 141 | 142 | create_db() { 143 | mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA 144 | } 145 | 146 | install_db() { 147 | 148 | if [ ${SKIP_DB_CREATE} = "true" ]; then 149 | return 0 150 | fi 151 | 152 | # parse DB_HOST for port or socket references 153 | local PARTS=(${DB_HOST//\:/ }) 154 | local DB_HOSTNAME=${PARTS[0]}; 155 | local DB_SOCK_OR_PORT=${PARTS[1]}; 156 | local EXTRA="" 157 | 158 | if ! [ -z $DB_HOSTNAME ] ; then 159 | if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then 160 | EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp" 161 | elif ! [ -z $DB_SOCK_OR_PORT ] ; then 162 | EXTRA=" --socket=$DB_SOCK_OR_PORT" 163 | elif ! [ -z $DB_HOSTNAME ] ; then 164 | EXTRA=" --host=$DB_HOSTNAME --protocol=tcp" 165 | fi 166 | fi 167 | 168 | # create database 169 | if [ $(mysql --user="$DB_USER" --password="$DB_PASS"$EXTRA --execute='show databases;' | grep ^$DB_NAME$) ] 170 | then 171 | echo "Reinstalling will delete the existing test database ($DB_NAME)" 172 | recreate_db $DELETE_EXISTING_DB 173 | else 174 | create_db 175 | fi 176 | } 177 | 178 | install_wp 179 | install_test_suite 180 | install_db 181 | -------------------------------------------------------------------------------- /includes/customizations/class-show-page-on-front.php: -------------------------------------------------------------------------------- 1 | is_main_query() || is_admin() || is_feed() ) { 66 | return; 67 | } 68 | 69 | if ( empty( $query->query[ Taxonomy::SLUG ] ) ) { 70 | return; 71 | } 72 | 73 | $brand_slug = $query->query[ Taxonomy::SLUG ]; 74 | $term = get_term_by( 'slug', $brand_slug, Taxonomy::SLUG ); 75 | 76 | if ( $term ) { 77 | $show_page_on_front = get_term_meta( $term->term_id, Show_Page_On_Front_Meta::get_key(), true ); 78 | if ( ! empty( $show_page_on_front ) ) { 79 | $page = get_page( $show_page_on_front ); 80 | if ( $page ) { 81 | $query->query = [ 'page_id' => $page->ID ]; 82 | $query->query_vars = $query->query; 83 | $query->parse_query(); 84 | self::$filtered = true; 85 | } 86 | } 87 | } 88 | } 89 | 90 | /** 91 | * Fixes the body classes when the term displays a page on front 92 | * 93 | * @param string[] $classes An array of body class names. 94 | * @param string[] $class An array of additional class names added to the body. 95 | * @return array 96 | */ 97 | public static function body_class( $classes, $class ) { 98 | $queried_object = get_queried_object(); 99 | if ( ! $queried_object instanceof \WP_Term || Taxonomy::SLUG !== $queried_object->taxonomy ) { 100 | return $classes; 101 | } 102 | 103 | $show_page_on_front = get_term_meta( $queried_object->term_id, Show_Page_On_Front_Meta::get_key(), true ); 104 | 105 | if ( ! $show_page_on_front ) { 106 | return $classes; 107 | } 108 | 109 | $classes_to_remove = [ 'archive', '-template-default', 'page-id-' . $queried_object->term_id ]; 110 | 111 | $classes = array_diff( $classes, $classes_to_remove ); 112 | 113 | $classes = array_merge( 114 | [ 115 | 'newspack-front-page', 116 | 'page-template-default', 117 | 'page-id-' . $show_page_on_front, 118 | ], 119 | $classes 120 | ); 121 | 122 | return $classes; 123 | } 124 | 125 | /** 126 | * Filters the template to use when displaying a page on front 127 | * 128 | * @param string $template The template file. 129 | * @return string 130 | */ 131 | public static function template_include( $template ) { 132 | if ( is_page() && self::is_filtered() ) { 133 | $template = get_front_page_template(); 134 | } 135 | 136 | return $template; 137 | } 138 | 139 | /** 140 | * Filters the page permalink 141 | * 142 | * @param string $permalink The page permalink. 143 | * @param int $page_id The page ID. 144 | * @return string 145 | */ 146 | public static function filter_page_link( $permalink, $page_id ) { 147 | $brand_id = self::get_brand_page_is_cover_for( $page_id ); 148 | if ( ! $brand_id ) { 149 | return $permalink; 150 | } 151 | 152 | $brand = get_term( $brand_id, Taxonomy::SLUG ); 153 | if ( ! $brand instanceof WP_Term ) { 154 | return $permalink; 155 | } 156 | 157 | return get_term_link( $brand, Taxonomy::SLUG ); 158 | } 159 | 160 | /** 161 | * Gets the front pages option 162 | * 163 | * @return array 164 | */ 165 | protected static function get_front_pages() { 166 | $front_pages = get_option( self::FRONT_PAGES_OPTION_KEY, [] ); 167 | if ( ! is_array( $front_pages ) ) { 168 | $front_pages = []; 169 | } 170 | return $front_pages; 171 | } 172 | 173 | /** 174 | * Updates the front pages option 175 | * 176 | * @param array $front_pages The front pages array. Keys are page IDs and values Brand IDs. 177 | * @return void 178 | */ 179 | protected static function update_front_pages( $front_pages ) { 180 | update_option( self::FRONT_PAGES_OPTION_KEY, $front_pages ); 181 | } 182 | 183 | /** 184 | * Updates the front pages option when a term meta is updated 185 | * 186 | * @param int $meta_id The Meta ID. 187 | * @param int $object_id The Object ID. 188 | * @param string $meta_key The Meta key. 189 | * @param string $meta_value The Meta value. 190 | * @return void 191 | */ 192 | public static function on_term_meta_update( $meta_id, $object_id, $meta_key, $meta_value ) { 193 | if ( '_show_page_on_front' === $meta_key ) { 194 | $front_pages = self::get_front_pages(); 195 | $pages_by_brands = array_flip( $front_pages ); 196 | 197 | if ( isset( $pages_by_brands[ $object_id ] ) ) { 198 | unset( $pages_by_brands[ $object_id ] ); 199 | } 200 | 201 | if ( ! empty( $meta_value ) ) { 202 | $pages_by_brands[ $object_id ] = (int) $meta_value; 203 | } 204 | 205 | // Doing array_flip twice also ensures an unique page by brand. 206 | self::update_front_pages( array_flip( $pages_by_brands ) ); 207 | } 208 | } 209 | 210 | /** 211 | * If a page is set as the front page for a brand, returns the brand ID 212 | * 213 | * @param int $page_id The page ID. 214 | * @return ?int The Brand ID. Null if the page is not set as the front page for any brand 215 | */ 216 | public static function get_brand_page_is_cover_for( $page_id ) { 217 | $front_pages = self::get_front_pages(); 218 | return isset( $front_pages[ $page_id ] ) ? $front_pages[ $page_id ] : null; 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /languages/newspack-multibranded-site.pot: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2025 Automattic 2 | # This file is distributed under the GPL3. 3 | msgid "" 4 | msgstr "" 5 | "Project-Id-Version: Newspack Multibranded Site 2.1.0\n" 6 | "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/project\n" 7 | "Last-Translator: FULL NAME \n" 8 | "Language-Team: LANGUAGE \n" 9 | "MIME-Version: 1.0\n" 10 | "Content-Type: text/plain; charset=UTF-8\n" 11 | "Content-Transfer-Encoding: 8bit\n" 12 | "POT-Creation-Date: 2025-11-03T18:32:37+00:00\n" 13 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 14 | "X-Generator: WP-CLI 2.12.0\n" 15 | "X-Domain: newspack-multibranded-site\n" 16 | 17 | #. Plugin Name of the plugin 18 | #: newspack-multibranded-site.php 19 | msgid "Newspack Multibranded Site" 20 | msgstr "" 21 | 22 | #. Description of the plugin 23 | #: newspack-multibranded-site.php 24 | msgid "Brand different content and sections of your site with unique colors and navigation." 25 | msgstr "" 26 | 27 | #. Author of the plugin 28 | #: newspack-multibranded-site.php 29 | msgid "Automattic" 30 | msgstr "" 31 | 32 | #. Author URI of the plugin 33 | #: newspack-multibranded-site.php 34 | msgid "https://newspack.com/" 35 | msgstr "" 36 | 37 | #: includes/admin/class-cat-primary-brand.php:44 38 | #: includes/admin/class-user-primary-brand.php:68 39 | msgid "Primary Brand" 40 | msgstr "" 41 | 42 | #: includes/admin/class-cat-primary-brand.php:47 43 | #: includes/admin/class-user-primary-brand.php:73 44 | #: dist/postPrimaryBrand.js:1 45 | #: src/post-primary-brand/index.js:78 46 | msgid "None" 47 | msgstr "" 48 | 49 | #: includes/admin/class-cat-primary-brand.php:55 50 | msgid "The primary brand defines what brand will be applied to the term's archive. (added by Newspack Multibranded site)" 51 | msgstr "" 52 | 53 | #. translators: %s: Brand name 54 | #: includes/admin/class-show-page-on-front.php:47 55 | #, php-format 56 | msgid "Front page for %s" 57 | msgstr "" 58 | 59 | #: includes/admin/class-user-primary-brand.php:62 60 | msgid "Multibranded site Options" 61 | msgstr "" 62 | 63 | #: includes/admin/class-user-primary-brand.php:63 64 | msgid "The user primary brand defines what brand will be applied to the user's archive." 65 | msgstr "" 66 | 67 | #: includes/class-admin.php:58 68 | #: includes/class-admin.php:59 69 | msgid "Multibranded site" 70 | msgstr "" 71 | 72 | #: includes/class-taxonomy.php:90 73 | msgctxt "taxonomy general name" 74 | msgid "Brands" 75 | msgstr "" 76 | 77 | #: includes/class-taxonomy.php:91 78 | msgctxt "taxonomy singular name" 79 | msgid "Brand" 80 | msgstr "" 81 | 82 | #: includes/class-taxonomy.php:92 83 | msgid "Search Brands" 84 | msgstr "" 85 | 86 | #: includes/class-taxonomy.php:93 87 | msgid "All Brands" 88 | msgstr "" 89 | 90 | #: includes/class-taxonomy.php:94 91 | msgid "Parent Brand" 92 | msgstr "" 93 | 94 | #: includes/class-taxonomy.php:95 95 | msgid "Parent Brand:" 96 | msgstr "" 97 | 98 | #: includes/class-taxonomy.php:96 99 | msgid "Edit Brand" 100 | msgstr "" 101 | 102 | #: includes/class-taxonomy.php:97 103 | msgid "Update Brand" 104 | msgstr "" 105 | 106 | #: includes/class-taxonomy.php:98 107 | msgid "Add New Brand" 108 | msgstr "" 109 | 110 | #: includes/class-taxonomy.php:99 111 | msgid "New Brand Name" 112 | msgstr "" 113 | 114 | #: includes/class-taxonomy.php:100 115 | #: dist/postPrimaryBrand.js:1 116 | #: src/post-primary-brand/index.js:71 117 | msgid "Brands" 118 | msgstr "" 119 | 120 | #: includes/meta/class-category-primary-brand.php:42 121 | msgid "The primary brand for a category" 122 | msgstr "" 123 | 124 | #: includes/meta/class-logo.php:32 125 | msgid "The ID of the attachment to be used as a logo for this brand" 126 | msgstr "" 127 | 128 | #: includes/meta/class-menus.php:32 129 | msgid "An array containing objects describing which menu should be displayed in which location" 130 | msgstr "" 131 | 132 | #: includes/meta/class-post-primary-brand.php:40 133 | msgid "The primary brand for a post, if a post belongs to more than one brand" 134 | msgstr "" 135 | 136 | #: includes/meta/class-show-page-on-front.php:32 137 | #: includes/meta/class-url.php:32 138 | msgid "Whether the brand URL should be at the root of the site (yes) or at the default taxonomy URL (no)" 139 | msgstr "" 140 | 141 | #: includes/meta/class-tag-primary-brand.php:42 142 | msgid "The primary brand for a tag" 143 | msgstr "" 144 | 145 | #: includes/meta/class-theme-colors.php:32 146 | msgid "An array of customizable colors defined by the theme. Each item has a name, which refers to the theme_mod name, and a value, which is a color hex code." 147 | msgstr "" 148 | 149 | #: includes/meta/class-user-primary-brand.php:40 150 | msgid "The primary brand for a user" 151 | msgstr "" 152 | 153 | #: dist/admin.js:1 154 | #: src/admin/views/brands/Brand.js:137 155 | msgid "Brand" 156 | msgstr "" 157 | 158 | #: dist/admin.js:1 159 | #: src/admin/views/brands/Brand.js:138 160 | msgid "Set your brand identity" 161 | msgstr "" 162 | 163 | #: dist/admin.js:1 164 | #: src/admin/views/brands/Brand.js:143 165 | msgid "Name" 166 | msgstr "" 167 | 168 | #: dist/admin.js:1 169 | #: src/admin/views/brands/Brand.js:152 170 | msgid "Logo" 171 | msgstr "" 172 | 173 | #: dist/admin.js:1 174 | #: src/admin/views/brands/Brand.js:161 175 | msgid "Colors" 176 | msgstr "" 177 | 178 | #: dist/admin.js:1 179 | #: src/admin/views/brands/Brand.js:162 180 | msgid "These are the colors you can customize for this brand in the active theme" 181 | msgstr "" 182 | 183 | #: dist/admin.js:1 184 | #: src/admin/views/brands/Brand.js:177 185 | msgid "Reset default color" 186 | msgstr "" 187 | 188 | #: dist/admin.js:1 189 | #: src/admin/views/brands/Brand.js:189 190 | msgid "Settings" 191 | msgstr "" 192 | 193 | #: dist/admin.js:1 194 | #: src/admin/views/brands/Brand.js:193 195 | msgid "URL Base" 196 | msgstr "" 197 | 198 | #: dist/admin.js:1 199 | #: src/admin/views/brands/Brand.js:196 200 | msgid "Homepage" 201 | msgstr "" 202 | 203 | #: dist/admin.js:1 204 | #: src/admin/views/brands/Brand.js:197 205 | msgid "Default" 206 | msgstr "" 207 | 208 | #: dist/admin.js:1 209 | #: src/admin/views/brands/Brand.js:205 210 | msgid "Slug" 211 | msgstr "" 212 | 213 | #: dist/admin.js:1 214 | #: src/admin/views/brands/Brand.js:217 215 | msgid "Show on Front" 216 | msgstr "" 217 | 218 | #: dist/admin.js:1 219 | #: src/admin/views/brands/Brand.js:220 220 | msgid "Latest posts" 221 | msgstr "" 222 | 223 | #: dist/admin.js:1 224 | #: src/admin/views/brands/Brand.js:221 225 | msgid "A page" 226 | msgstr "" 227 | 228 | #: dist/admin.js:1 229 | #: src/admin/views/brands/Brand.js:227 230 | msgid "Homepage URL" 231 | msgstr "" 232 | 233 | #: dist/admin.js:1 234 | #: src/admin/views/brands/Brand.js:231 235 | msgid "Select a Page" 236 | msgstr "" 237 | 238 | #: dist/admin.js:1 239 | #: src/admin/views/brands/Brand.js:247 240 | msgid "Menus" 241 | msgstr "" 242 | 243 | #: dist/admin.js:1 244 | #: src/admin/views/brands/Brand.js:248 245 | msgid "Customize the menus for this brand" 246 | msgstr "" 247 | 248 | #: dist/admin.js:1 249 | #: src/admin/views/brands/Brand.js:258 250 | msgid "Same as site" 251 | msgstr "" 252 | 253 | #: dist/admin.js:1 254 | #: src/admin/views/brands/Brand.js:270 255 | msgid "Save" 256 | msgstr "" 257 | 258 | #: dist/admin.js:1 259 | #: src/admin/views/brands/Brand.js:273 260 | msgid "Cancel" 261 | msgstr "" 262 | 263 | #: dist/postPrimaryBrand.js:1 264 | #: src/post-primary-brand/index.js:74 265 | msgid "Primary brand" 266 | msgstr "" 267 | 268 | #: dist/postPrimaryBrand.js:1 269 | #: src/post-primary-brand/index.js:91 270 | msgid "Manage Brands" 271 | msgstr "" 272 | -------------------------------------------------------------------------------- /includes/class-taxonomy.php: -------------------------------------------------------------------------------- 1 | _x( 'Brands', 'taxonomy general name', 'newspack-multibranded-site' ), 91 | 'singular_name' => _x( 'Brand', 'taxonomy singular name', 'newspack-multibranded-site' ), 92 | 'search_items' => __( 'Search Brands', 'newspack-multibranded-site' ), 93 | 'all_items' => __( 'All Brands', 'newspack-multibranded-site' ), 94 | 'parent_item' => __( 'Parent Brand', 'newspack-multibranded-site' ), 95 | 'parent_item_colon' => __( 'Parent Brand:', 'newspack-multibranded-site' ), 96 | 'edit_item' => __( 'Edit Brand', 'newspack-multibranded-site' ), 97 | 'update_item' => __( 'Update Brand', 'newspack-multibranded-site' ), 98 | 'add_new_item' => __( 'Add New Brand', 'newspack-multibranded-site' ), 99 | 'new_item_name' => __( 'New Brand Name', 'newspack-multibranded-site' ), 100 | 'menu_name' => __( 'Brands', 'newspack-multibranded-site' ), 101 | ); 102 | $params = array( 103 | 'labels' => $labels, 104 | 'hierarchical' => true, // Just to get the checkbox UI. 105 | 'publicly_queryable' => true, 106 | 'show_in_nav_menus' => true, 107 | 'show_in_menu' => false, 108 | 'show_ui' => true, 109 | 'show_admin_column' => true, 110 | 'show_in_rest' => true, 111 | 'query_var' => true, 112 | 'capabilities' => array( 113 | 'manage_terms' => 'manage_options', 114 | 'edit_terms' => 'manage_options', 115 | 'delete_terms' => 'manage_options', 116 | 'assign_terms' => 'edit_posts', 117 | ), 118 | ); 119 | register_taxonomy( self::SLUG, self::get_post_types(), $params ); 120 | 121 | // Initialize metadata. 122 | Meta\Url::init(); 123 | Meta\Show_Page_On_Front::init(); 124 | Meta\Post_Primary_Brand::init(); 125 | Meta\Logo::init(); 126 | Meta\Theme_Colors::init(); 127 | Meta\Menus::init(); 128 | Meta\User_Primary_Brand::init(); 129 | Meta\Tag_Primary_Brand::init(); 130 | Meta\Category_Primary_Brand::init(); 131 | } 132 | 133 | /** 134 | * Get the current brand based on a post. 135 | * 136 | * If a post has is of a supported post type and has only one brand, it will return this brand, otherwise it will return null. 137 | * 138 | * @param int|WP_Post $post_or_post_id The Post object or the post id. 139 | * @return ?WP_Term The current brand for the post. 140 | */ 141 | public static function get_current_brand_for_post( $post_or_post_id ) { 142 | // Account for Brands with page on front. 143 | if ( $post_or_post_id instanceof WP_Term ) { 144 | return self::get_current_brand_for_term( $post_or_post_id ); 145 | } 146 | 147 | $post = $post_or_post_id instanceof \WP_Post ? $post_or_post_id : get_post( $post_or_post_id ); 148 | 149 | if ( ! in_array( $post->post_type, self::POST_TYPES, true ) ) { 150 | return; 151 | } 152 | 153 | // Check if post is assigned to only one brand. 154 | $terms = wp_get_post_terms( $post->ID, self::SLUG ); 155 | 156 | if ( 1 === count( $terms ) ) { 157 | return $terms[0]; 158 | } 159 | 160 | // Check if post has a primary brand. 161 | $post_primary_brand = get_post_meta( $post->ID, self::PRIMARY_META_KEY, true ); 162 | 163 | if ( $post_primary_brand ) { 164 | $term = get_term( $post_primary_brand, self::SLUG ); 165 | if ( $term instanceof WP_Term ) { 166 | return $term; 167 | } 168 | } 169 | 170 | // Check if post is a cover page for a brand. 171 | if ( 'page' === $post->post_type ) { 172 | $brand = Show_Page_On_Front::get_brand_page_is_cover_for( $post->ID ); 173 | if ( $brand ) { 174 | $term = get_term( $brand, self::SLUG ); 175 | if ( $term instanceof WP_Term ) { 176 | return $term; 177 | } 178 | } 179 | } 180 | 181 | // Check if post is assigned to a brand through a category. 182 | $categories = wp_get_post_categories( $post->ID ); 183 | $category_brand = null; 184 | foreach ( $categories as $category ) { 185 | $brand = self::get_current_brand_for_term( $category ); 186 | if ( $brand ) { 187 | if ( ! $category_brand || $category_brand->term_id === $brand->term_id ) { 188 | $category_brand = $brand; 189 | continue; 190 | } 191 | 192 | // Found more than one eligible brand, return null. 193 | return null; 194 | } 195 | } 196 | 197 | return $category_brand; 198 | } 199 | 200 | /** 201 | * Get the current brand based on a term. 202 | * 203 | * If a term is a brand, it will return this brand 204 | * 205 | * @param int|WP_Term $term_or_term_id The Term object or the term id. 206 | * @return ?WP_Term The current brand for the post. 207 | */ 208 | public static function get_current_brand_for_term( $term_or_term_id ) { 209 | $term = $term_or_term_id instanceof WP_Term ? $term_or_term_id : get_term( $term_or_term_id ); 210 | if ( self::SLUG === $term->taxonomy ) { 211 | return $term; 212 | } 213 | if ( in_array( $term->taxonomy, [ 'category', 'post_tag' ], true ) ) { 214 | return self::recursive_search_term_primary_brand( $term ); 215 | } 216 | } 217 | 218 | /** 219 | * Finds the primary brand for a term, searching recursively through ancestors. 220 | * 221 | * @param WP_Term $term The Term. 222 | * @return ?WP_Term The primary brand for the term. 223 | */ 224 | protected static function recursive_search_term_primary_brand( WP_Term $term ) { 225 | $primary_brand = get_term_meta( $term->term_id, self::PRIMARY_META_KEY, true ); 226 | if ( $primary_brand ) { 227 | $brand = get_term( $primary_brand, self::SLUG ); 228 | if ( $brand instanceof WP_Term ) { 229 | return $brand; 230 | } 231 | } 232 | if ( $term->parent ) { 233 | $parent = get_term( $term->parent, $term->taxonomy ); 234 | if ( $parent instanceof WP_Term ) { 235 | return self::recursive_search_term_primary_brand( $parent ); 236 | } 237 | } 238 | } 239 | 240 | /** 241 | * Get the current brand based on an author. 242 | * 243 | * If the author has a custom primary brand, it will return this brand 244 | * 245 | * @param int $author_id The author ID. 246 | * @return ?WP_Term The current brand for the post. 247 | */ 248 | public static function get_current_brand_for_author( $author_id ) { 249 | $author_brand = get_user_meta( $author_id, self::PRIMARY_META_KEY, true ); 250 | if ( ! $author_brand ) { 251 | return; 252 | } 253 | $brand = get_term( $author_brand, self::SLUG ); 254 | if ( $brand instanceof WP_Term ) { 255 | return $brand; 256 | } 257 | } 258 | 259 | /** 260 | * Determines and stores the current brand depending on the current context. 261 | * 262 | * @return void 263 | */ 264 | public static function determine_current_brand() { 265 | global $wp_query; 266 | if ( $wp_query->is_singular() ) { 267 | self::$current_brand = self::get_current_brand_for_post( get_queried_object() ); 268 | } elseif ( $wp_query->is_tax() || $wp_query->is_category() || $wp_query->is_tag() ) { 269 | self::$current_brand = self::get_current_brand_for_term( get_queried_object() ); 270 | } elseif ( $wp_query->is_author() ) { 271 | self::$current_brand = self::get_current_brand_for_author( get_queried_object_id() ); 272 | } else { 273 | self::$current_brand = null; 274 | } 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /src/admin/views/brands/Brand.js: -------------------------------------------------------------------------------- 1 | /* global newspack_aux_data */ 2 | 3 | import apiFetch from '@wordpress/api-fetch'; 4 | import { addQueryArgs, cleanForSlug } from '@wordpress/url'; 5 | import { Fragment, useState, useEffect } from '@wordpress/element'; 6 | import { __ } from '@wordpress/i18n'; 7 | import { useParams } from 'react-router-dom'; 8 | 9 | import { 10 | Card, 11 | Grid, 12 | Button, 13 | SectionHeader, 14 | TextControl, 15 | ImageUpload, 16 | ColorPicker, 17 | SelectControl, 18 | RadioControl, 19 | withWizardScreen, 20 | hooks, 21 | } from 'newspack-components'; 22 | 23 | import './style.scss'; 24 | 25 | const Brand = ( { brands = [], saveBrand, fetchLogoAttachment } ) => { 26 | const [ brand, updateBrand ] = hooks.useObjectState( { slug: '', meta: { _custom_url: 'yes' } } ); 27 | const [ publicPages, setPublicPages ] = useState( [] ); 28 | const [ showOnFrontSelect, setShowOnFrontSelect ] = useState( 'no' ); 29 | 30 | const { brandId } = useParams(); 31 | const selectedBrand = brands.find( ( { id } ) => id === Number( brandId ) ); 32 | 33 | const registeredThemeColors = newspack_aux_data.theme_colors; 34 | const menuLocations = newspack_aux_data.menu_locations; 35 | const availableMenus = newspack_aux_data.menus; 36 | 37 | useEffect( () => { 38 | if ( selectedBrand ) { 39 | updateBrand( selectedBrand ); 40 | if ( ! isNaN( selectedBrand.meta._logo ) ) { 41 | fetchLogoAttachment( Number( brandId ), selectedBrand.meta._logo ); 42 | } 43 | setShowOnFrontSelect( selectedBrand.meta._show_page_on_front ? 'yes' : 'no' ); 44 | } 45 | }, [ selectedBrand ] ); 46 | 47 | const getThemeColor = colorName => { 48 | const color = brand.meta._theme_colors?.find( c => colorName === c.name )?.color; 49 | return color ? color : registeredThemeColors.find( c => colorName === c.theme_mod_name )?.default; 50 | }; 51 | 52 | const hasCustomThemeColor = colorName => { 53 | const color = brand.meta._theme_colors?.find( c => colorName === c.name )?.color; 54 | return color ? true : false; 55 | }; 56 | 57 | const setThemeColor = ( name, color ) => { 58 | const themeColors = brand?.meta._theme_colors ? brand?.meta._theme_colors : []; 59 | const colorIndex = themeColors.findIndex( _color => name === _color.name ); 60 | let updatedThemeColors = []; 61 | 62 | if ( ! color && colorIndex > -1 ) { 63 | // Resetting default color. 64 | themeColors.splice( colorIndex, 1 ); 65 | updatedThemeColors = themeColors; 66 | } else if ( color && colorIndex > -1 ) { 67 | // Updating color. 68 | updatedThemeColors = themeColors.map( _color => ( name === _color.name ? { ..._color, color } : _color ) ); 69 | } else if ( color && colorIndex === -1 ) { 70 | // Adding color. 71 | updatedThemeColors = [ ...themeColors, { name, color } ]; 72 | } else if ( ! color && colorIndex === -1 ) { 73 | // should not happen. 74 | return; 75 | } 76 | 77 | return updateBrand( { 78 | meta: { 79 | _theme_colors: updatedThemeColors, 80 | }, 81 | } ); 82 | }; 83 | 84 | const updateSlugFromName = e => { 85 | if ( '' === brand.slug ) { 86 | updateBrand( { slug: cleanForSlug( e.target.value ) } ); 87 | } 88 | }; 89 | 90 | const updateShowOnFront = value => { 91 | if ( 'no' === value ) { 92 | updateBrand( { meta: { ...brand.meta, _show_page_on_front: 0 } } ); 93 | } 94 | setShowOnFrontSelect( value ); 95 | }; 96 | 97 | const updateMenus = ( location, menu ) => { 98 | const menus = brand.meta._menus ? brand.meta._menus : []; 99 | const menuIndex = menus.findIndex( _menu => location === _menu.location ); 100 | 101 | const updatedMenus = 102 | menuIndex > -1 ? menus.map( _menu => ( location === _menu.location ? { ..._menu, menu } : _menu ) ) : [ ...menus, { location, menu } ]; 103 | 104 | return updateBrand( { 105 | meta: { 106 | _menus: updatedMenus, 107 | }, 108 | } ); 109 | }; 110 | 111 | const baseUrl = `${ newspack_aux_data.site }/${ 'no' === brand.meta._custom_url ? 'brand/' : '' }`; 112 | 113 | const fetchPublicPages = () => { 114 | // Limiting to 100 pages, just in case. 115 | apiFetch( { 116 | path: addQueryArgs( '/wp/v2/pages', { per_page: 100, orderby: 'title', order: 'asc' } ), 117 | } ).then( setPublicPages ); 118 | }; 119 | 120 | useEffect( fetchPublicPages, [] ); 121 | 122 | // Brand is valid when it has a name, and if a page is selected to be shown in front, the page should be selected. 123 | const isBrandValid = 124 | 0 < brand.name?.length && ( 'no' === showOnFrontSelect || ( 'yes' === showOnFrontSelect && 0 < brand.meta._show_page_on_front ) ); 125 | 126 | const findSelectedMenu = location => { 127 | if ( ! brand.meta._menus ) { 128 | return 0; 129 | } 130 | const selectedMenu = brand.meta._menus.find( menu => menu.location === location ); 131 | return selectedMenu ? selectedMenu.menu : 0; 132 | }; 133 | 134 | return ( 135 | 136 | 140 | 141 | 142 | 148 | 149 | 150 | updateBrand( { meta: { _logo } } ) } 155 | /> 156 | 157 | 158 | 159 | { registeredThemeColors && ( 160 | 164 | ) } 165 | 166 | { registeredThemeColors && 167 | registeredThemeColors.map( color => { 168 | return ( 169 | 170 | 174 | { color.label } 175 | { hasCustomThemeColor( color.theme_mod_name ) && ( 176 | setThemeColor( color.theme_mod_name, '' ) }> 177 | { __( 'Reset default color', 'newspack-multibranded-site' ) } 178 | 179 | ) } 180 | 181 | } 182 | color={ getThemeColor( color.theme_mod_name ) } 183 | onChange={ newColor => setThemeColor( color.theme_mod_name, newColor ) } 184 | /> 185 | 186 | ); 187 | } ) } 188 | 189 | 190 | 191 | updateBrand( { meta: { _custom_url } } ) } 200 | /> 201 | 202 | { baseUrl } 203 | 211 | 212 | 213 | 214 | 215 | updateShowOnFront( value ) } 224 | /> 225 | { 'yes' === showOnFrontSelect && ( 226 | ( { 236 | label: page.title.rendered, 237 | value: Number( page.id ), 238 | } ) ), 239 | ] } 240 | onChange={ _show_page_on_front => updateBrand( { meta: { _show_page_on_front } } ) } 241 | required 242 | /> 243 | ) } 244 | 245 | 246 | 250 | 251 | { Object.keys( menuLocations ).map( location => ( 252 | updateMenus( location, menuId ) } 265 | /> 266 | ) ) } 267 | 268 | 269 | saveBrand( Number( brandId ), brand ) }> 270 | { __( 'Save', 'newspack-multibranded-site' ) } 271 | 272 | 273 | { __( 'Cancel', 'newspack-multibranded-site' ) } 274 | 275 | 276 | 277 | ); 278 | }; 279 | 280 | export default withWizardScreen( Brand ); 281 | -------------------------------------------------------------------------------- /tests/unit-tests/test-taxonomy.php: -------------------------------------------------------------------------------- 1 | assertTrue( taxonomy_exists( Taxonomy::SLUG ), 'The taxonomy is not registered' ); 21 | } 22 | 23 | /** 24 | * Test get_current_brand_for_post 25 | */ 26 | public function test_get_current_brand_for_post() { 27 | $brand1 = $this->factory->term->create_and_get( array( 'taxonomy' => Taxonomy::SLUG ) ); 28 | $brand2 = $this->factory->term->create_and_get( array( 'taxonomy' => Taxonomy::SLUG ) ); 29 | 30 | $post = $this->factory->post->create_and_get( array( 'post_title' => 'Post 1' ) ); 31 | $page = $this->factory->post->create_and_get( 32 | array( 33 | 'post_title' => 'Post 2', 34 | 'post_type' => 'page', 35 | ) 36 | ); 37 | $other_pt = $this->factory->post->create_and_get( 38 | array( 39 | 'post_title' => 'Post 3', 40 | 'post_type' => 'nav_menu', 41 | ) 42 | ); 43 | 44 | $this->assertSame( null, Taxonomy::get_current_brand_for_post( $post->ID ), 'Null should be returned if none is set' ); 45 | 46 | wp_set_post_terms( $post->ID, $brand1->term_id, Taxonomy::SLUG ); 47 | $this->assertSame( $brand1->term_id, Taxonomy::get_current_brand_for_post( $post->ID )->term_id, 'Related brand should be returned if ony one is added' ); 48 | 49 | wp_set_post_terms( $post->ID, [ $brand1->term_id, $brand2->term_id ], Taxonomy::SLUG ); 50 | $this->assertSame( null, Taxonomy::get_current_brand_for_post( $post->ID ), 'Null should be returned if more than on brand is set' ); 51 | 52 | wp_set_post_terms( $page->ID, $brand2->term_id, Taxonomy::SLUG ); 53 | $this->assertSame( $brand2->term_id, Taxonomy::get_current_brand_for_post( $page->ID )->term_id, 'Related brand should be returned if ony one is added' ); 54 | 55 | wp_set_post_terms( $other_pt->ID, $brand1->term_id, Taxonomy::SLUG ); 56 | $this->assertSame( null, Taxonomy::get_current_brand_for_post( $other_pt->ID ), 'Null should be returned for other post types' ); 57 | } 58 | 59 | /** 60 | * Test fallback logic for posts that are in a branded category but don't have a brand assigned. 61 | */ 62 | public function test_get_current_brand_for_post_fallback() { 63 | $brand = $this->factory->term->create_and_get( array( 'taxonomy' => Taxonomy::SLUG ) ); 64 | $brand2 = $this->factory->term->create_and_get( array( 'taxonomy' => Taxonomy::SLUG ) ); 65 | 66 | $category_with_brand = $this->factory->term->create_and_get( array( 'taxonomy' => 'category' ) ); 67 | add_term_meta( $category_with_brand->term_id, Taxonomy::PRIMARY_META_KEY, $brand->term_id ); 68 | 69 | $category_with_brand2 = $this->factory->term->create_and_get( array( 'taxonomy' => 'category' ) ); 70 | add_term_meta( $category_with_brand2->term_id, Taxonomy::PRIMARY_META_KEY, $brand2->term_id ); 71 | 72 | $category_with_brand2_2 = $this->factory->term->create_and_get( array( 'taxonomy' => 'category' ) ); 73 | add_term_meta( $category_with_brand2_2->term_id, Taxonomy::PRIMARY_META_KEY, $brand2->term_id ); 74 | 75 | $category_with_parent_branded = $this->factory->term->create_and_get( 76 | array( 77 | 'taxonomy' => 'category', 78 | 'parent' => $brand2->term_id, 79 | ) 80 | ); 81 | 82 | $category_without_brand = $this->factory->term->create_and_get( array( 'taxonomy' => 'category' ) ); 83 | 84 | $post = $this->factory->post->create_and_get( array( 'post_title' => 'Post 1' ) ); 85 | wp_set_post_categories( $post->ID, [ $category_with_brand->term_id ] ); 86 | 87 | $post2 = $this->factory->post->create_and_get( array( 'post_title' => 'Post 2' ) ); 88 | wp_set_post_categories( $post2->ID, [ $category_without_brand->term_id ] ); 89 | 90 | $post_one_branded_one_unbranded = $this->factory->post->create_and_get( array( 'post_title' => 'Post one branded one unbranded' ) ); 91 | wp_set_post_categories( $post_one_branded_one_unbranded->ID, [ $category_without_brand->term_id, $category_with_brand->term_id ] ); 92 | 93 | $post_with_two_branded_cats = $this->factory->post->create_and_get( array( 'post_title' => 'Post 3' ) ); 94 | wp_set_post_categories( $post_with_two_branded_cats->ID, [ $category_with_brand->term_id, $category_with_brand2->term_id ] ); 95 | 96 | $post_with_parent_and_child_branded = $this->factory->post->create_and_get( array( 'post_title' => 'Post with parent and child branded cats' ) ); 97 | wp_set_post_categories( $post_with_parent_and_child_branded->ID, [ $category_with_brand2->term_id, $category_with_parent_branded->term_id ] ); 98 | 99 | $post_with_two_cats_in_the_same_brand = $this->factory->post->create_and_get( array( 'post_title' => 'Post with two cats related to the same brand' ) ); 100 | wp_set_post_categories( $post_with_two_cats_in_the_same_brand->ID, [ $category_with_brand2->term_id, $category_with_brand2_2->term_id ] ); 101 | 102 | $this->assertSame( $brand->term_id, Taxonomy::get_current_brand_for_post( $post->ID )->term_id, 'Related brand should be returned for posts in a branded category that are not explicitly branded' ); 103 | $this->assertSame( null, Taxonomy::get_current_brand_for_post( $post2->ID ), 'Null should be returned for unbranded posts in an unbranded category' ); 104 | $this->assertSame( $brand->term_id, Taxonomy::get_current_brand_for_post( $post_one_branded_one_unbranded->ID )->term_id, 'Related brand should be returned for posts in multiple categories but when only one is branded' ); 105 | $this->assertSame( null, Taxonomy::get_current_brand_for_post( $post_with_two_branded_cats->ID ), 'Null should be returned if post is assigned to more than one branded category' ); 106 | $this->assertSame( $brand2->term_id, Taxonomy::get_current_brand_for_post( $post_with_parent_and_child_branded->ID )->term_id, 'Brand should be returned if post is assigned to more than one branded category, but if they all are related to the same brand' ); 107 | $this->assertSame( $brand2->term_id, Taxonomy::get_current_brand_for_post( $post_with_two_cats_in_the_same_brand->ID )->term_id, 'Brand should be returned if post is assigned to more than one branded category, but if they all are related to the same brand' ); 108 | } 109 | 110 | /** 111 | * Test get_current_brand_for_term 112 | */ 113 | public function test_get_current_brand_for_term() { 114 | $brand1 = $this->factory->term->create_and_get( array( 'taxonomy' => Taxonomy::SLUG ) ); 115 | $brand2 = $this->factory->term->create_and_get( array( 'taxonomy' => 'category' ) ); 116 | 117 | $this->assertSame( $brand1->term_id, Taxonomy::get_current_brand_for_term( $brand1->term_id )->term_id, 'Term should be returned if is a brand' ); 118 | $this->assertSame( null, Taxonomy::get_current_brand_for_term( $brand2->term_id ), 'Null should be returned if other taxonomy' ); 119 | } 120 | 121 | /** 122 | * Tests get current brand and determine current brand methods 123 | */ 124 | public function test_determine_current_brand() { 125 | $author_wo_brand = $this->factory->user->create_and_get(); 126 | $author_with_brand = $this->factory->user->create_and_get(); 127 | $author_with_invalid_brand = $this->factory->user->create_and_get(); 128 | $author_with_invalid_brand_2 = $this->factory->user->create_and_get(); 129 | $brand1 = $this->factory->term->create_and_get( array( 'taxonomy' => Taxonomy::SLUG ) ); 130 | $brand2 = $this->factory->term->create_and_get( array( 'taxonomy' => Taxonomy::SLUG ) ); 131 | $brand_with_page_on_front = $this->factory->term->create_and_get( array( 'taxonomy' => Taxonomy::SLUG ) ); 132 | $simple_category = $this->factory->term->create_and_get( array( 'taxonomy' => 'category' ) ); 133 | $category_with_brand = $this->factory->term->create_and_get( array( 'taxonomy' => 'category' ) ); 134 | $simple_category_child = $this->factory->term->create_and_get( 135 | array( 136 | 'taxonomy' => 'category', 137 | 'parent' => $simple_category->term_id, 138 | ) 139 | ); 140 | $category_with_brand_child = $this->factory->term->create_and_get( 141 | array( 142 | 'taxonomy' => 'category', 143 | 'parent' => $category_with_brand->term_id, 144 | ) 145 | ); 146 | $simple_tag = $this->factory->term->create_and_get( array( 'taxonomy' => 'post_tag' ) ); 147 | $tag_with_brand = $this->factory->term->create_and_get( array( 'taxonomy' => 'post_tag' ) ); 148 | $post = $this->factory->post->create_and_get( 149 | array( 150 | 'post_title' => 'Post 1', 151 | 'post_author' => $author_wo_brand->ID, 152 | ) 153 | ); 154 | $post_2_brands = $this->factory->post->create_and_get( 155 | array( 156 | 'post_title' => 'Post 2', 157 | 'post_author' => $author_wo_brand->ID, 158 | ) 159 | ); 160 | $post_2_brands_and_primary = $this->factory->post->create_and_get( 161 | array( 162 | 'post_title' => 'Post 2 and primary', 163 | 'post_author' => $author_wo_brand->ID, 164 | ) 165 | ); 166 | $page = $this->factory->post->create_and_get( 167 | array( 168 | 'post_title' => 'Post 2', 169 | 'post_type' => 'page', 170 | ) 171 | ); 172 | $page2 = $this->factory->post->create_and_get( 173 | array( 174 | 'post_title' => 'Page 2', 175 | 'post_type' => 'page', 176 | ) 177 | ); 178 | 179 | wp_set_post_terms( $post->ID, $brand1->term_id, Taxonomy::SLUG ); 180 | wp_set_post_terms( $post->ID, $simple_category->term_id, 'category' ); 181 | wp_set_post_terms( $post_2_brands->ID, [ $brand1->term_id, $brand2->term_id ], Taxonomy::SLUG ); 182 | wp_set_post_terms( $post_2_brands_and_primary->ID, [ $brand1->term_id, $brand2->term_id ], Taxonomy::SLUG ); 183 | add_post_meta( $post_2_brands_and_primary->ID, Taxonomy::PRIMARY_META_KEY, $brand2->term_id ); 184 | wp_set_post_terms( $post_2_brands->ID, $simple_category->term_id, 'category' ); 185 | 186 | add_user_meta( $author_with_brand->ID, Taxonomy::PRIMARY_META_KEY, $brand1->term_id ); 187 | add_user_meta( $author_with_invalid_brand->ID, Taxonomy::PRIMARY_META_KEY, 999999 ); 188 | add_user_meta( $author_with_invalid_brand_2->ID, Taxonomy::PRIMARY_META_KEY, $simple_category->term_id ); 189 | 190 | add_term_meta( $category_with_brand->term_id, Taxonomy::PRIMARY_META_KEY, $brand1->term_id ); 191 | add_term_meta( $tag_with_brand->term_id, Taxonomy::PRIMARY_META_KEY, $brand1->term_id ); 192 | 193 | add_term_meta( $brand_with_page_on_front->term_id, Show_Page_On_Front::get_key(), $page2->ID ); 194 | 195 | // home. 196 | $this->go_to( '/' ); 197 | $this->assertSame( null, Taxonomy::get_current(), 'Null should be returned if on home' ); 198 | 199 | // search. 200 | $this->go_to( '/?s=asd' ); 201 | $this->assertSame( null, Taxonomy::get_current(), 'Null should be returned if on search' ); 202 | 203 | // Brand archive. 204 | $this->go_to( get_term_link( $brand1 ) ); 205 | $this->assertSame( $brand1->term_id, Taxonomy::get_current()->term_id, 'Brand should be returned if on brand archive' ); 206 | 207 | // Post with one brand. 208 | $this->go_to( get_permalink( $post->ID ) ); 209 | $this->assertSame( $brand1->term_id, Taxonomy::get_current()->term_id, 'Brand should be returned if on post with one brand' ); 210 | 211 | // Post with two brands. 212 | $this->go_to( get_permalink( $post_2_brands->ID ) ); 213 | $this->assertSame( null, Taxonomy::get_current(), 'Null should be returned if on post with two brands' ); 214 | 215 | // Post with two brands and primary. 216 | $this->go_to( get_permalink( $post_2_brands_and_primary->ID ) ); 217 | $this->assertSame( $brand2->term_id, Taxonomy::get_current()->term_id, 'Primary brand should be returned if on post with two brands and primary' ); 218 | 219 | // author archive. 220 | $this->go_to( get_author_posts_url( $author_wo_brand->ID ) ); 221 | $this->assertSame( null, Taxonomy::get_current(), 'Null should be returned if on author archive' ); 222 | 223 | $this->go_to( get_author_posts_url( $author_with_brand->ID ) ); 224 | $this->assertSame( $brand1->term_id, Taxonomy::get_current()->term_id, 'Brand should be returned if on author archive when primary brand is set' ); 225 | 226 | $this->go_to( get_author_posts_url( $author_with_invalid_brand->ID ) ); 227 | $this->assertSame( null, Taxonomy::get_current(), 'Null should be returned if on author archive when primary brand is invalid' ); 228 | 229 | $this->go_to( get_author_posts_url( $author_with_invalid_brand_2->ID ) ); 230 | $this->assertSame( null, Taxonomy::get_current(), 'Null should be returned if on author archive when primary brand is invalid' ); 231 | 232 | // categories and tags. 233 | $this->go_to( get_term_link( $simple_category ) ); 234 | $this->assertSame( null, Taxonomy::get_current(), 'Null should be returned if on category without primary brand' ); 235 | 236 | $this->go_to( get_term_link( $simple_tag ) ); 237 | $this->assertSame( null, Taxonomy::get_current(), 'Null should be returned if on tag without primary brand' ); 238 | 239 | // category with brand. 240 | $this->go_to( get_term_link( $category_with_brand ) ); 241 | $this->assertSame( $brand1->term_id, Taxonomy::get_current()->term_id, 'Brand should be returned if on category with brand' ); 242 | 243 | // child category of a category with brand. 244 | $this->go_to( get_term_link( $category_with_brand_child ) ); 245 | $this->assertSame( $brand1->term_id, Taxonomy::get_current()->term_id, 'Brand should be returned if on child category of a category with brand' ); 246 | 247 | // child category of a category without brand. 248 | $this->go_to( get_term_link( $simple_category_child ) ); 249 | $this->assertSame( null, Taxonomy::get_current(), 'Null should be returned if on child category of a category without brand' ); 250 | 251 | // tag with brand. 252 | $this->go_to( get_term_link( $tag_with_brand ) ); 253 | $this->assertSame( $brand1->term_id, Taxonomy::get_current()->term_id, 'Brand should be returned if on tag with brand' ); 254 | 255 | // Brand with page on front. 256 | $this->go_to( get_term_link( $brand_with_page_on_front ) ); 257 | $this->assertSame( $brand_with_page_on_front->term_id, Taxonomy::get_current()->term_id, 'Brand should be returned if on brand with page on front' ); 258 | 259 | // Page set to be the front page of a brand. 260 | $this->go_to( get_permalink( $page2->ID ) ); 261 | $this->assertSame( $brand_with_page_on_front->term_id, Taxonomy::get_current()->term_id, 'Page that is set to be used as front page of a brand should load that brand' ); 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [2.2.0](https://github.com/Automattic/newspack-multibranded-site/compare/v2.1.0...v2.2.0) (2025-11-24) 2 | 3 | 4 | ### Features 5 | 6 | * Merge pull request [#87](https://github.com/Automattic/newspack-multibranded-site/issues/87) from Automattic/trunk ([0155b5b](https://github.com/Automattic/newspack-multibranded-site/commit/0155b5be2d2cae92232d087b8e33e76aef030091)) 7 | 8 | # [2.1.0](https://github.com/Automattic/newspack-multibranded-site/compare/v2.0.6...v2.1.0) (2025-08-25) 9 | 10 | 11 | ### Bug Fixes 12 | 13 | * phpunit tests compat with php 8.3 ([#80](https://github.com/Automattic/newspack-multibranded-site/issues/80)) ([ac2076d](https://github.com/Automattic/newspack-multibranded-site/commit/ac2076d9cad38353bb67ba72a19d43d67d826016)) 14 | 15 | 16 | ### Features 17 | 18 | * add i18n ([#81](https://github.com/Automattic/newspack-multibranded-site/issues/81)) ([2e9bf9c](https://github.com/Automattic/newspack-multibranded-site/commit/2e9bf9ca5cb87c113f3a27f40198a9c47da9e951)) 19 | 20 | ## [2.0.6](https://github.com/Automattic/newspack-multibranded-site/compare/v2.0.5...v2.0.6) (2025-06-02) 21 | 22 | 23 | ### Bug Fixes 24 | 25 | * add customization class to fix site title url ([#73](https://github.com/Automattic/newspack-multibranded-site/issues/73)) ([aff3784](https://github.com/Automattic/newspack-multibranded-site/commit/aff37847cffbfaf07e1f7cc25584d1f4a1dcd76d)) 26 | * **admin:** campaigns admin page detection ([#74](https://github.com/Automattic/newspack-multibranded-site/issues/74)) ([b5382f8](https://github.com/Automattic/newspack-multibranded-site/commit/b5382f85530ca852ff946d9686182caed5a06745)) 27 | 28 | ## [2.0.5](https://github.com/Automattic/newspack-multibranded-site/compare/v2.0.4...v2.0.5) (2025-05-14) 29 | 30 | 31 | ### Bug Fixes 32 | 33 | * force alpha build ([1e2b0de](https://github.com/Automattic/newspack-multibranded-site/commit/1e2b0de184ed8326906b893076f6256865f3d70c)) 34 | 35 | ## [2.0.4](https://github.com/Automattic/newspack-multibranded-site/compare/v2.0.3...v2.0.4) (2025-03-31) 36 | 37 | 38 | ### Bug Fixes 39 | 40 | * **posts-listing:** don't show empty brand dropdown ([#69](https://github.com/Automattic/newspack-multibranded-site/issues/69)) ([98bf17a](https://github.com/Automattic/newspack-multibranded-site/commit/98bf17af7ca508d82d175cbd447e27a1fdfa15cb)) 41 | 42 | ## [2.0.3](https://github.com/Automattic/newspack-multibranded-site/compare/v2.0.2...v2.0.3) (2024-12-16) 43 | 44 | 45 | ### Bug Fixes 46 | 47 | * load text domain on init hook ([#66](https://github.com/Automattic/newspack-multibranded-site/issues/66)) ([f7d8261](https://github.com/Automattic/newspack-multibranded-site/commit/f7d8261b6d6e1172d4442a8413cfcb7d7ab2d70f)) 48 | 49 | ## [2.0.2](https://github.com/Automattic/newspack-multibranded-site/compare/v2.0.1...v2.0.2) (2024-11-11) 50 | 51 | 52 | ### Bug Fixes 53 | 54 | * missing globals ([#60](https://github.com/Automattic/newspack-multibranded-site/issues/60)) ([10c3f17](https://github.com/Automattic/newspack-multibranded-site/commit/10c3f17ae2823be1efd91177e26a9ade50122cd3)) 55 | * **wp-6.7:** update radio control styles [#58](https://github.com/Automattic/newspack-multibranded-site/issues/58) ([2c586cd](https://github.com/Automattic/newspack-multibranded-site/commit/2c586cd95b4dc94602a325caedfd7315395db444)) 56 | 57 | ## [2.0.1](https://github.com/Automattic/newspack-multibranded-site/compare/v2.0.0...v2.0.1) (2024-10-09) 58 | 59 | 60 | ### Bug Fixes 61 | 62 | * Preserve RSS feed for brands with page on front ([46f8524](https://github.com/Automattic/newspack-multibranded-site/commit/46f85248ecca7a10ec98a385ea4376082f59f7fc)) 63 | * Preserve RSS feed for brands with page on front Merge pull request [#55](https://github.com/Automattic/newspack-multibranded-site/issues/55) from Automattic/hotfix/rss-feed ([eb117c4](https://github.com/Automattic/newspack-multibranded-site/commit/eb117c451d8f5a6ee38394eb0fb8d5fb12502a8b)) 64 | 65 | # [2.0.0](https://github.com/Automattic/newspack-multibranded-site/compare/v1.4.0...v2.0.0) (2024-08-13) 66 | 67 | 68 | ### Bug Fixes 69 | 70 | * do not break wp_update_term ([#51](https://github.com/Automattic/newspack-multibranded-site/issues/51)) ([70c0ea4](https://github.com/Automattic/newspack-multibranded-site/commit/70c0ea414aa87e2386865011767cb8252b430ef7)) 71 | * update dependencies to support `@wordpress/scripts` ([#45](https://github.com/Automattic/newspack-multibranded-site/issues/45)) ([de1a172](https://github.com/Automattic/newspack-multibranded-site/commit/de1a1725911c7a3d2b711099f24f12f11b58ec5e)) 72 | 73 | 74 | ### Features 75 | 76 | * **ga:** add brand as custom parameter to GA4 ([bff4df3](https://github.com/Automattic/newspack-multibranded-site/commit/bff4df371e5c29d5cd412f7c874547e31f1b0f5c)) 77 | 78 | 79 | ### BREAKING CHANGES 80 | 81 | * Updates dependencies for compatibility with WordPress 6.6.*, but breaks JS in WordPress 6.5.* and below. If you need support for WP 6.5.*, please do not upgrade to this new major version. 82 | 83 | * chore: refactor for newspack-scripts dependency updates 84 | 85 | * chore: update newspack-scripts to v5.6.0-alpha.3 86 | 87 | * chore: add .stylelintrc.js 88 | 89 | * chore: update newspack-scripts to v5.6.0-alpha.4 90 | 91 | * fix: add missing Prettier config files 92 | 93 | * chore: update newspack-scripts to 5.6.0-alpha.5 94 | 95 | * chore: update newspack-scripts to v5.6.0-alpha.7 96 | 97 | * fix: format SCSS 98 | 99 | * chore: update newspack-scripts to v5.6.0-alpha.8 100 | 101 | * fix: phpcs errors 102 | 103 | * chore: update newspack-components to v3.0.0 104 | 105 | * fix: phpcs error 106 | 107 | * chore: bump newspack-scripts to v5.5.2 108 | 109 | # [1.4.0](https://github.com/Automattic/newspack-multibranded-site/compare/v1.3.0...v1.4.0) (2024-07-01) 110 | 111 | 112 | ### Bug Fixes 113 | 114 | * update newspack-scripts to v5.5.1 ([#47](https://github.com/Automattic/newspack-multibranded-site/issues/47)) ([89f82e8](https://github.com/Automattic/newspack-multibranded-site/commit/89f82e84e5094d3b0c9037e4a5dc9e6de86ff190)) 115 | 116 | 117 | ### Features 118 | 119 | * ensure regenerator-runtime is available (for WP 6.6) ([4dff4ef](https://github.com/Automattic/newspack-multibranded-site/commit/4dff4ef170cdf315218c4302b1b1e805e6a12f9f)) 120 | 121 | # [1.3.0](https://github.com/Automattic/newspack-multibranded-site/compare/v1.2.0...v1.3.0) (2024-04-08) 122 | 123 | 124 | ### Features 125 | 126 | * **ci:** add epic/* release workflow and rename `master` to `trunk` ([#41](https://github.com/Automattic/newspack-multibranded-site/issues/41)) ([290e390](https://github.com/Automattic/newspack-multibranded-site/commit/290e390af56a8af20a90d952c2d6596714a5045c)) 127 | 128 | # [1.3.0-alpha.1](https://github.com/Automattic/newspack-multibranded-site/compare/v1.2.0...v1.3.0-alpha.1) (2024-02-08) 129 | 130 | 131 | ### Features 132 | 133 | * **ci:** add epic/* release workflow and rename `master` to `trunk` ([#41](https://github.com/Automattic/newspack-multibranded-site/issues/41)) ([290e390](https://github.com/Automattic/newspack-multibranded-site/commit/290e390af56a8af20a90d952c2d6596714a5045c)) 134 | 135 | # [1.2.0](https://github.com/Automattic/newspack-multibranded-site/compare/v1.1.0...v1.2.0) (2023-12-11) 136 | 137 | 138 | ### Bug Fixes 139 | 140 | * release process ([5a93b1f](https://github.com/Automattic/newspack-multibranded-site/commit/5a93b1f5ba4449f9ed85a1635f62cd77ce8ee6e5)) 141 | 142 | 143 | ### Features 144 | 145 | * use brand in pages set as brand fronts ([#29](https://github.com/Automattic/newspack-multibranded-site/issues/29)) ([7ab39d6](https://github.com/Automattic/newspack-multibranded-site/commit/7ab39d69801ac8639675106f307a75e88ff9511a)) 146 | 147 | # [1.2.0-alpha.1](https://github.com/Automattic/newspack-multibranded-site/compare/v1.1.0...v1.2.0-alpha.1) (2023-11-30) 148 | 149 | 150 | ### Bug Fixes 151 | 152 | * release process ([5a93b1f](https://github.com/Automattic/newspack-multibranded-site/commit/5a93b1f5ba4449f9ed85a1635f62cd77ce8ee6e5)) 153 | 154 | 155 | ### Features 156 | 157 | * use brand in pages set as brand fronts ([#29](https://github.com/Automattic/newspack-multibranded-site/issues/29)) ([7ab39d6](https://github.com/Automattic/newspack-multibranded-site/commit/7ab39d69801ac8639675106f307a75e88ff9511a)) 158 | 159 | # [1.1.0](https://github.com/Automattic/newspack-multibranded-site/compare/v1.0.0...v1.1.0) (2023-11-30) 160 | 161 | 162 | ### Bug Fixes 163 | 164 | * release process ([b42ff85](https://github.com/Automattic/newspack-multibranded-site/commit/b42ff85805ec68782124e56e914d58d2e93f945f)) 165 | * update help site URL ([#32](https://github.com/Automattic/newspack-multibranded-site/issues/32)) ([6369b10](https://github.com/Automattic/newspack-multibranded-site/commit/6369b101af36044fb6cf9b4f223fe04fbc8cf767)) 166 | 167 | 168 | ### Features 169 | 170 | * add auto updater ([#31](https://github.com/Automattic/newspack-multibranded-site/issues/31)) ([86bf6a3](https://github.com/Automattic/newspack-multibranded-site/commit/86bf6a3c8d1f659c4f92bc8d5dd307a061bd6074)) 171 | * change menu position ([0e46fbf](https://github.com/Automattic/newspack-multibranded-site/commit/0e46fbfc8f40251dc927744862ef07e7d1532516)) 172 | * filter posts by brand in admin ([#28](https://github.com/Automattic/newspack-multibranded-site/issues/28)) ([9094911](https://github.com/Automattic/newspack-multibranded-site/commit/9094911de161628deb823e668eb814c4610f6074)) 173 | * trigger new release ([33a173e](https://github.com/Automattic/newspack-multibranded-site/commit/33a173e95674c537ad34a6b4d7b83317a3fa3302)) 174 | 175 | # [1.2.0](https://github.com/Automattic/newspack-multibranded-site/compare/v1.1.0...v1.2.0) (2023-11-29) 176 | 177 | 178 | ### Features 179 | 180 | * trigger new release ([33a173e](https://github.com/Automattic/newspack-multibranded-site/commit/33a173e95674c537ad34a6b4d7b83317a3fa3302)) 181 | 182 | # [1.1.0](https://github.com/Automattic/newspack-multibranded-site/compare/v1.0.1...v1.1.0) (2023-11-29) 183 | 184 | 185 | ### Features 186 | 187 | * add auto updater ([#31](https://github.com/Automattic/newspack-multibranded-site/issues/31)) ([86bf6a3](https://github.com/Automattic/newspack-multibranded-site/commit/86bf6a3c8d1f659c4f92bc8d5dd307a061bd6074)) 188 | * change menu position ([0e46fbf](https://github.com/Automattic/newspack-multibranded-site/commit/0e46fbfc8f40251dc927744862ef07e7d1532516)) 189 | * filter posts by brand in admin ([#28](https://github.com/Automattic/newspack-multibranded-site/issues/28)) ([9094911](https://github.com/Automattic/newspack-multibranded-site/commit/9094911de161628deb823e668eb814c4610f6074)) 190 | 191 | ## [1.0.1](https://github.com/Automattic/newspack-multibranded-site/compare/v1.0.0...v1.0.1) (2023-10-11) 192 | 193 | 194 | ### Bug Fixes 195 | 196 | * update help site URL ([#32](https://github.com/Automattic/newspack-multibranded-site/issues/32)) ([6369b10](https://github.com/Automattic/newspack-multibranded-site/commit/6369b101af36044fb6cf9b4f223fe04fbc8cf767)) 197 | 198 | # 1.0.0 (2023-08-17) 199 | 200 | 201 | ### Bug Fixes 202 | 203 | * add missing dependency ([b31a7ba](https://github.com/Automattic/newspack-multibranded-site/commit/b31a7ba476b88d529f2cc2e643a3cbd09958f71a)) 204 | * **autoload:** fix autoloader path ([3ca2286](https://github.com/Automattic/newspack-multibranded-site/commit/3ca2286bd9513b53915766bc6f2cc2d6483c1372)) 205 | * better use of selector ([80fa247](https://github.com/Automattic/newspack-multibranded-site/commit/80fa24716c9c47e07800fe471e4731ae4b38ffbd)) 206 | * **Brand:** add confirmation message and delete brand ([#11](https://github.com/Automattic/newspack-multibranded-site/issues/11)) ([9ebe51e](https://github.com/Automattic/newspack-multibranded-site/commit/9ebe51e44a362f8981311ff3779bb373297aa08d)) 207 | * hook registration ([49341b7](https://github.com/Automattic/newspack-multibranded-site/commit/49341b7ef79f0b59e2e2160d7429e8e4206f56ed)) 208 | * ignore menus when value is zero ([0b741b6](https://github.com/Automattic/newspack-multibranded-site/commit/0b741b6d40da7762e7e30cff3b20d6fff802865e)) 209 | * js lint directory ([435ee70](https://github.com/Automattic/newspack-multibranded-site/commit/435ee70ab534a0f2c3a3de49bb8cd6135972eb0c)) 210 | * lint ([6491074](https://github.com/Automattic/newspack-multibranded-site/commit/649107423817ac933b910ce9b0694720e55a8f3e)) 211 | * load right template when page is on front ([c3f7088](https://github.com/Automattic/newspack-multibranded-site/commit/c3f7088ce81bdba3ddea26c39aaf93249cd5954d)) 212 | * only add post meta to associated post types ([#20](https://github.com/Automattic/newspack-multibranded-site/issues/20)) ([72131be](https://github.com/Automattic/newspack-multibranded-site/commit/72131be76c7d3d3f4c12e39a69d37f530365275e)) 213 | * php linting ([514b759](https://github.com/Automattic/newspack-multibranded-site/commit/514b7591093f1c5dcdfc0fed292ce77bc5927e5a)) 214 | * rename option ([68dd2e6](https://github.com/Automattic/newspack-multibranded-site/commit/68dd2e6cd77e2e9519ddb78398d69d84964a99dc)) 215 | * template tags calls ([580b5ea](https://github.com/Automattic/newspack-multibranded-site/commit/580b5ea7048730fd0276a07f3df994704c860f4c)) 216 | * typo in function name ([4e31e26](https://github.com/Automattic/newspack-multibranded-site/commit/4e31e26de92d20891305ea3d6a2c1c8115d01019)) 217 | * unsued hook on admin ([8a449ee](https://github.com/Automattic/newspack-multibranded-site/commit/8a449eec7364d3b9c932094d29e287e9190a1880)) 218 | * use filter and filter out yoast ([62a2772](https://github.com/Automattic/newspack-multibranded-site/commit/62a2772b9ee1f10905a5c9df40d036f0b5778677)) 219 | 220 | 221 | ### Features 222 | 223 | * adapt filter based on final changes to popups plugin ([ae5b2d4](https://github.com/Automattic/newspack-multibranded-site/commit/ae5b2d4442da32b7df36662c889da4cb46c2c1c5)) 224 | * Add brands for campaign prompts ([c39f91c](https://github.com/Automattic/newspack-multibranded-site/commit/c39f91cd16f57cfcaf331d33965e49298b461022)) 225 | * add category and tags primary brand ([760193b](https://github.com/Automattic/newspack-multibranded-site/commit/760193b035fb67fe7f402df80ccc5f859cdf65c1)) 226 | * Add custom logo option ([dfc7b43](https://github.com/Automattic/newspack-multibranded-site/commit/dfc7b4350c55da2a4d4242c4c0414f8c218604b4)) 227 | * Add menu option and customization ([c851c5e](https://github.com/Automattic/newspack-multibranded-site/commit/c851c5e223cf27f5e46051335c4f371e3f2dd4b8)) 228 | * Add menu option and customization ([7dadd30](https://github.com/Automattic/newspack-multibranded-site/commit/7dadd3037277ea8d522683e7afcbd2b5638053c3)) 229 | * add menu options to the UI ([9febdb4](https://github.com/Automattic/newspack-multibranded-site/commit/9febdb440208a59bcc1957f641d5311f6356e374)) 230 | * add post primary brand UI ([9d21083](https://github.com/Automattic/newspack-multibranded-site/commit/9d210835ee1ffc978a32f40ad495f22aa0f29f71)) 231 | * add post primary brand UI [#12](https://github.com/Automattic/newspack-multibranded-site/issues/12) ([e7e602b](https://github.com/Automattic/newspack-multibranded-site/commit/e7e602b3f76851b1baa767acc9576f1413ea7db4)) 232 | * Add theme colors ([c4572d1](https://github.com/Automattic/newspack-multibranded-site/commit/c4572d10891ea9bb957606dbb998ea4a0683f071)) 233 | * **brand:** add clear button to theme color picker ([#21](https://github.com/Automattic/newspack-multibranded-site/issues/21)) ([deff3b9](https://github.com/Automattic/newspack-multibranded-site/commit/deff3b9685331f1a3d5c558efa1d955cd6bc2c41)) 234 | * **Brand:** add/edit a brand ([#7](https://github.com/Automattic/newspack-multibranded-site/issues/7)) ([51d50c3](https://github.com/Automattic/newspack-multibranded-site/commit/51d50c331687c55ba7737df711bf4dc539df8c1b)) 235 | * **brands:** add brands list UI ([dc0979c](https://github.com/Automattic/newspack-multibranded-site/commit/dc0979cab2f8cbeff95ee4fb35ba8a1016efc61c)) 236 | * check for post primary brand ([c8db9c3](https://github.com/Automattic/newspack-multibranded-site/commit/c8db9c38067cdf0759e96404be78f11dd1d372c3)) 237 | * convert it to a stand-alone sidebar panel ([4e64456](https://github.com/Automattic/newspack-multibranded-site/commit/4e6445630697057eff03e63ea4d1443a4d923d16)) 238 | * **customization:** add brand identifier in body class ([bce6906](https://github.com/Automattic/newspack-multibranded-site/commit/bce6906f846a6e245484f1a984c45cbc96f30026)) 239 | * filter the blog name ([0a64b98](https://github.com/Automattic/newspack-multibranded-site/commit/0a64b9869bccfb6a286db1fc6d4e34c70361fc5c)) 240 | * filters custom logo link ([#18](https://github.com/Automattic/newspack-multibranded-site/issues/18)) ([eedf593](https://github.com/Automattic/newspack-multibranded-site/commit/eedf593228416fe604b2886c41ac93fa62b49938)) 241 | * manage prompt conflicts ([#19](https://github.com/Automattic/newspack-multibranded-site/issues/19)) ([5bc0631](https://github.com/Automattic/newspack-multibranded-site/commit/5bc06310663381ddb0f4ed2f3a236308a88de2b1)) 242 | * Merge pull request [#24](https://github.com/Automattic/newspack-multibranded-site/issues/24) from Automattic/test/ci-release ([5545986](https://github.com/Automattic/newspack-multibranded-site/commit/5545986519cdd9864145e2015a48384d0c6d70dd)) 243 | * refactor post meta into its own class and register meta ([0213cfb](https://github.com/Automattic/newspack-multibranded-site/commit/0213cfb6168197a36dc12a771b7800121903bbad)) 244 | * refactor primary brand into an option ([b71876f](https://github.com/Automattic/newspack-multibranded-site/commit/b71876f8831cf2f0280dad93bbdc8d37157295e2)) 245 | * remove current brand support ([0c6d2c6](https://github.com/Automattic/newspack-multibranded-site/commit/0c6d2c68b9fa6740bc5830192a12f77f2c33a3af)) 246 | * update newspack-components ([5e8694c](https://github.com/Automattic/newspack-multibranded-site/commit/5e8694cce55f16820a19f2121edf53781e75a50e)) 247 | --------------------------------------------------------------------------------
{ __( 'Create brands to enhance your readers experience.', 'newspack' ) }
55 | 56 |