Migrated Admin code into monorepo package

refs https://github.com/TryGhost/Toolbox/issues/365

- this migrates all the Admin code and history into the monorepo 🚀
This commit is contained in:
Daniel Lockyer 2022-08-03 15:50:36 +02:00
commit b29cd27792
No known key found for this signature in database
GPG Key ID: D21186F0B47295AD
1845 changed files with 159226 additions and 0 deletions

240
ghost/admin/.csscomb.json Normal file
View File

@ -0,0 +1,240 @@
{
"remove-empty-rulesets": true,
"always-semicolon": true,
"color-case": "lower",
"block-indent": " ",
"color-shorthand": true,
"element-case": "lower",
"eof-newline": true,
"leading-zero": true,
"quotes": "double",
"space-before-colon": "",
"space-after-colon": " ",
"space-before-combinator": " ",
"space-after-combinator": " ",
"space-between-declarations": "\n",
"space-before-opening-brace": " ",
"space-after-opening-brace": "\n",
"space-after-selector-delimiter": "\n",
"space-before-selector-delimiter": "",
"space-before-closing-brace": "\n",
"strip-spaces": true,
"tab-size": 4,
"unitless-zero": true,
"sort-order": [ [
"content",
"visibility",
"position",
"top",
"right",
"bottom",
"left",
"z-index",
"order",
"flex",
"flex-grow",
"flex-shrink",
"flex-basis",
"align-self",
"display",
"flex-flow",
"flex-direction",
"justify-content",
"align-items",
"align-content",
"flex-wrap",
"flex-order",
"flex-pack",
"flex-align",
"float",
"clear",
"box-sizing",
"width",
"height",
"min-width",
"min-height",
"max-width",
"max-height",
"overflow",
"overflow-x",
"overflow-y",
"clip",
"margin",
"margin-top",
"margin-right",
"margin-bottom",
"margin-left",
"padding",
"padding-top",
"padding-right",
"padding-bottom",
"padding-left",
"outline",
"outline-width",
"outline-style",
"outline-color",
"outline-offset",
"border",
"border-spacing",
"border-collapse",
"border-width",
"border-style",
"border-color",
"border-top",
"border-top-width",
"border-top-style",
"border-top-color",
"border-right",
"border-right-width",
"border-right-style",
"border-right-color",
"border-bottom",
"border-bottom-width",
"border-bottom-style",
"border-bottom-color",
"border-left",
"border-left-width",
"border-left-style",
"border-left-color",
"border-image",
"border-image-source",
"border-image-slice",
"border-image-width",
"border-image-outset",
"border-image-repeat",
"border-top-image",
"border-right-image",
"border-bottom-image",
"border-left-image",
"border-corner-image",
"border-top-left-image",
"border-top-right-image",
"border-bottom-right-image",
"border-bottom-left-image",
"table-layout",
"caption-side",
"empty-cells",
"list-style",
"list-style-position",
"list-style-type",
"list-style-image",
"quotes",
"counter-increment",
"counter-reset",
"vertical-align",
"stroke",
"fill",
"stroke-width",
"stroke-opacity",
"color",
"font",
"font-family",
"font-size",
"line-height",
"font-weight",
"font-style",
"font-variant",
"font-size-adjust",
"font-stretch",
"text-rendering",
"font-feature-settings",
"letter-spacing",
"hyphens",
"text-align",
"text-align-last",
"text-decoration",
"text-emphasis",
"text-emphasis-position",
"text-emphasis-style",
"text-emphasis-color",
"text-indent",
"text-justify",
"text-outline",
"text-transform",
"text-wrap",
"text-overflow",
"text-overflow-ellipsis",
"text-overflow-mode",
"text-shadow",
"white-space",
"word-spacing",
"word-wrap",
"word-break",
"tab-size",
"user-select",
"src",
"resize",
"cursor",
"nav-index",
"nav-up",
"nav-right",
"nav-down",
"nav-left",
"background",
"filter:progid:DXImageTransform.Microsoft.AlphaImageLoader",
"background-color",
"background-image",
"background-size",
"background-attachment",
"background-position",
"background-position-x",
"background-position-y",
"background-clip",
"background-origin",
"background-repeat",
"border-radius",
"border-top-left-radius",
"border-top-right-radius",
"border-bottom-right-radius",
"border-bottom-left-radius",
"box-decoration-break",
"box-shadow",
"opacity",
"filter:progid:DXImageTransform.Microsoft.Alpha(Opacity",
"filter",
"transition",
"transition-delay",
"transition-timing-function",
"transition-duration",
"transition-property",
"transform",
"transform-origin",
"animation",
"animation-name",
"animation-duration",
"animation-play-state",
"animation-timing-function",
"animation-delay",
"animation-iteration-count",
"animation-direction",
"animation-fill-mode",
"pointer-events",
"unicode-bidi",
"direction",
"columns",
"column-span",
"column-width",
"column-count",
"column-fill",
"column-gap",
"column-rule",
"column-rule-width",
"column-rule-style",
"column-rule-color",
"break-before",
"break-inside",
"break-after",
"page-break-before",
"page-break-inside",
"page-break-after",
"orphans",
"widows",
"zoom",
"max-zoom",
"min-zoom",
"user-zoom",
"orientation",
"-webkit-overflow-scrolling",
"-ms-overflow-scrolling"
] ]
}

26
ghost/admin/.editorconfig Normal file
View File

@ -0,0 +1,26 @@
# http://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 4
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.hbs]
insert_final_newline = false
[{package,bower}.json]
indent_size = 2
[*.{diff,md}]
trim_trailing_whitespace = false
[*.yml]
indent_size = 2
[Makefile]
indent_style = tab

11
ghost/admin/.ember-cli Normal file
View File

@ -0,0 +1,11 @@
{
"component-structure": "flat",
"component-class": "@glimmer/component",
/**
Ember CLI sends analytics information by default. The data is completely
anonymous, but there are times when you might want to disable this behavior.
Setting `disableAnalytics` to true will prevent any data from being sent.
*/
"disableAnalytics": true
}

20
ghost/admin/.eslintignore Normal file
View File

@ -0,0 +1,20 @@
# unconventional js
/blueprints/*/files/
/vendor/
# compiled output
/dist/
/tmp/
# dependencies
/bower_components/
/node_modules/
# misc
/coverage/
.eslintcache
# ember-try
/.node_modules.ember-try/
/bower.json.ember-try
/package.json.ember-try

54
ghost/admin/.eslintrc.js Normal file
View File

@ -0,0 +1,54 @@
/* eslint-env node */
module.exports = {
root: true,
parser: '@babel/eslint-parser',
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
allowImportExportEverywhere: false,
ecmaFeatures: {
globalReturn: false,
legacyDecorators: true,
jsx: true
},
requireConfigFile: false,
babelOptions: {
plugins: [
'@babel/plugin-proposal-class-properties',
['@babel/plugin-proposal-decorators', {legacy: true}],
'babel-plugin-transform-react-jsx'
]
}
},
plugins: [
'ghost',
'react'
],
extends: [
'plugin:ghost/ember'
],
rules: {
'no-shadow': ['error'],
// TODO: migrate away from accessing controller in routes
'ghost/ember/no-controller-access-in-routes': 'off',
// TODO: enable once we're fully on octane 🏎
'ghost/ember/no-assignment-of-untracked-properties-used-in-tracking-contexts': 'off',
'ghost/ember/no-actions-hash': 'off',
'ghost/ember/no-classic-classes': 'off',
'ghost/ember/no-classic-components': 'off',
'ghost/ember/require-tagless-components': 'off',
'ghost/ember/no-component-lifecycle-hooks': 'off',
// disable linting of `this.get` until there's a reliable autofix
'ghost/ember/use-ember-get-and-set': 'off',
// disable linting of mixins until we migrate away
'ghost/ember/no-mixins': 'off',
'ghost/ember/no-new-mixins': 'off',
'react/jsx-uses-react': 'error',
'react/jsx-uses-vars': 'error'
}
};

72
ghost/admin/.github/CONTRIBUTING.md vendored Normal file
View File

@ -0,0 +1,72 @@
# Contributing to Ghost
For **help**, **support**, **questions** and **ideas** please use **[our forum](https://forum.ghost.org)** 🚑.
---
## Where to Start
If you're a developer looking to contribute, but you're not sure where to begin: Check out the [good first issue](https://github.com/TryGhost/Ghost/labels/good%20first%20issue) label on Github, which contains small piece of work that have been specifically flagged as being friendly to new contributors.
After that, if you're looking for something a little more challenging to sink your teeth into, there's a broader [help wanted](https://github.com/TryGhost/Ghost/labels/help%20wanted) label encompassing issues which need some love.
If you've got an idea for a new feature, please start by suggesting it in the [forum](https://forum.ghost.org), as adding new features to Ghost first requires generating consensus around a design and spec.
## Working on Ghost Core
If you're going to work on Ghost core you'll need to go through a slightly more involved install and setup process than the usual Ghost CLI version.
First you'll need to fork both [Ghost](https://github.com/tryghost/ghost) and [Ghost-Admin](https://github.com/tryghost/ghost-admin) to your personal Github account, and then follow the detailed [install from source](https://ghost.org/docs/install/source/) setup guide.
### Branching Guide
`master` on the main repository always contains the latest changes. This means that it is WIP for the next minor version and should NOT be considered stable. Stable versions are tagged using [semantic versioning](http://semver.org/).
On your local repository, you should always work on a branch to make keeping up-to-date and submitting pull requests easier, but in most cases you should submit your pull requests to `master`. Where necessary, for example if multiple people are contributing on a large feature, or if a feature requires a database change, we make use of feature branches.
### Commit Messages
We have a handful of simple standards for commit messages which help us to generate readable changelogs. Please follow this wherever possible and mention the associated issue number.
- **1st line:** Max 80 character summary written in past tense
- **2nd line:** [Always blank]
- **3rd line:** `refs/closes #000` or `no issue`
- **4th line:** Whatever you want. Any extra details can be included from here
If your change is **user-facing** please prepend the first line of your commit with **an emoji key**.
We are following [gitmoji](https://gitmoji.carloscuesta.me/).
**Main emojis we are using:**
- ✨ Feature
- 🎨 Improvement / change
- 🐛 Bug Fix
- 💡 Anything else flagged to users or whoever is writing release notes
Good commit message examples: [one](https://github.com/TryGhost/Ghost/commit/61db6defde3b10a4022c86efac29cf15ae60983f), [two](https://github.com/TryGhost/Ghost/commit/b392d1925a9f961d7b4bf781ee86393a7773ed4b) and [three](https://github.com/TryGhost/Ghost/commit/e4807a779c28a754e3f8ae871a26a8aad12ca9a9).
### Submitting Pull Requests
We aim to merge any straightforward, well-understood bug fixes or improvements immediately, as long as they pass our tests (run `yarn test` to check locally). We generally dont merge new features and larger changes without prior discussion with the core product team for tech/design specification.
Please provide plenty of context and reasoning around your changes, to help us merge quickly. Closing an already open issue is our preferred workflow. If your PR gets out of date, we may ask you to rebase as you are more familiar with your changes than we will be.
---
## Contributor License Agreement
By contributing your code to Ghost you grant the Ghost Foundation a non-exclusive, irrevocable, worldwide, royalty-free, sublicenseable, transferable license under all of Your relevant intellectual property rights (including copyright, patent, and any other rights), to use, copy, prepare derivative works of, distribute and publicly perform and display the Contributions on any licensing terms, including without limitation:
(a) open source licenses like the MIT license; and (b) binary, proprietary, or commercial licenses. Except for the licenses granted herein, You reserve all right, title, and interest in and to the Contribution.
You confirm that you are able to grant us these rights. You represent that You are legally entitled to grant the above license. If Your employer has rights to intellectual property that You create, You represent that You have received permission to make the Contributions on behalf of that employer, or that Your employer has waived such rights for the Contributions.
You represent that the Contributions are Your original works of authorship, and to Your knowledge, no other person claims, or has the right to claim, any right in any invention or patent related to the Contributions. You also represent that You are not legally obligated, whether by entering into an agreement or otherwise, in any way that conflicts with the terms of this license.
The Ghost Foundation acknowledges that, except as explicitly described in this Agreement, any Contribution which you provide is on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.

View File

@ -0,0 +1,9 @@
Got some code for us? Awesome 🎊!
Please include a description of your change & check your PR against this list, thanks!
- [ ] There's a clear use-case for this code change
- [ ] Commit message has a short title & references relevant issues
- [ ] The build will pass (run `ember test` from the repo root - will be `core/admin` if working from the submodule in Ghost).
More info can be found by clicking the "guidelines for contributing" link above.

96
ghost/admin/.github/workflows/test.yml vendored Normal file
View File

@ -0,0 +1,96 @@
name: Test Suite
on:
pull_request:
push:
branches:
- "v4.*"
- "5.0"
- "multiple-newsletters"
- main
- "renovate/*"
jobs:
prod-build:
runs-on: ubuntu-18.04
if: github.event_name == 'push' || (github.event_name == 'pull_request' && !startsWith(github.head_ref, 'renovate/'))
name: Production Build
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: "14.17.0"
- run: yarn
- run: grunt shell:ember:prod
- uses: daniellockyer/action-slack-build@master
if: failure() && github.event_name == 'push' && github.ref == 'refs/heads/main'
with:
status: ${{ job.status }}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
lint:
runs-on: ubuntu-18.04
if: github.event_name == 'push' || (github.event_name == 'pull_request' && !startsWith(github.head_ref, 'renovate/'))
name: Lint
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: "14.17.0"
- run: yarn
- run: yarn lint:js
- run: yarn lint:hbs
- uses: daniellockyer/action-slack-build@master
if: failure() && github.event_name == 'push' && github.ref == 'refs/heads/main'
with:
status: ${{ job.status }}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
test:
runs-on: ubuntu-18.04
if: github.event_name == 'push' || (github.event_name == 'pull_request' && !startsWith(github.head_ref, 'renovate/'))
strategy:
matrix:
browser: ["Firefox", "Chrome"]
name: ${{ matrix.browser }}
env:
MOZ_HEADLESS: 1
JOBS: 1
CI: true
COVERAGE: true
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: "14.17.0"
- run: yarn
- run: yarn test
env:
BROWSER: ${{ matrix.browser }}
# Merge coverage reports and upload
- run: yarn ember coverage-merge
- uses: codecov/codecov-action@v3
- uses: daniellockyer/action-slack-build@master
if: failure() && github.event_name == 'push' && github.ref == 'refs/heads/main'
with:
status: ${{ job.status }}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
canary:
runs-on: ubuntu-18.04
needs: [lint, test, prod-build]
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
name: Canary
steps:
- name: Invoke Canary Build
uses: benc-uk/workflow-dispatch@v1
with:
workflow: Canary Build
repo: TryGhost/Ghost
token: ${{ secrets.WORKFLOW_TOKEN }}

63
ghost/admin/.gitignore vendored Normal file
View File

@ -0,0 +1,63 @@
b-cov
*.seed
*.log
*.csv
*.dat
*.out
*.pid
*.gz
pids
logs
results
npm-debug.log*
yarn-error.log
.nvmrc
.bowerrc
.idea/*
*.iml
*.sublime-*
projectFilesBackup
.DS_Store
# vim-related
[._]*.s[a-w][a-z]
[._]s[a-w][a-z]
*.un~
Session.vim
.netrwhist
.vimrc
*~
# TernJS
.tern-project
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist
/dist-test
/tmp
# dependencies
/node_modules
/bower_components
# IDE
.idea/*
*.iml
*.sublime-*
.vscode/*
# misc
/.env*
/.pnp*
/.eslintcache
/connect.lock
/coverage/
/libpeerconnection.log
/testem.log
/concat-stats-for
jsconfig.json

947
ghost/admin/.lint-todo Normal file
View File

@ -0,0 +1,947 @@
add|ember-template-lint|no-action|21|19|21|19|1cf358e2bf9d6fbb6abdc967c327ac9236e817dc|1658102400000|1668474000000|1673658000000|app/components/gh-benefit-item.hbs
add|ember-template-lint|no-action|22|22|22|22|b05a3bfe0536cd1cacb3a5803246be6f16857425|1658102400000|1668474000000|1673658000000|app/components/gh-benefit-item.hbs
add|ember-template-lint|no-action|24|23|24|23|cb8946eb596d861488d6f957a8cb9d8be47d6dbd|1658102400000|1668474000000|1673658000000|app/components/gh-benefit-item.hbs
add|ember-template-lint|no-action|35|49|35|49|3b24461a9c5dc4b6f39e0fcce071fcfde7944812|1658102400000|1668474000000|1673658000000|app/components/gh-benefit-item.hbs
add|ember-template-lint|no-action|39|52|39|52|2c005989f1aa2542a49c3a89330e445d0016efef|1658102400000|1668474000000|1673658000000|app/components/gh-benefit-item.hbs
add|ember-template-lint|no-passed-in-event-handlers|21|12|21|12|4069dec45ac2a31d440780c914b2307ad1c6ea5f|1658102400000|1668474000000|1673658000000|app/components/gh-benefit-item.hbs
add|ember-template-lint|no-passed-in-event-handlers|22|12|22|12|e53f64794fdd0fe8c8b027d1831942d7c78c503b|1658102400000|1668474000000|1673658000000|app/components/gh-benefit-item.hbs
add|ember-template-lint|require-iframe-title|1|0|1|0|d1c9631d150af53ca33b16c8c280c9d815bf43da|1658102400000|1668474000000|1673658000000|app/components/gh-billing-iframe.hbs
add|ember-template-lint|no-action|2|54|2|54|8618d17e29821f45d8809ad2d6cf6053b825f7fe|1658102400000|1668474000000|1673658000000|app/components/gh-billing-update-button.hbs
add|ember-template-lint|no-triple-curlies|1|0|1|0|26e1d2a3fcc165e3eb94cec903dc0fbff0e26e0e|1658102400000|1668474000000|1673658000000|app/components/gh-blog-url.hbs
add|ember-template-lint|no-invalid-interactive|72|72|72|72|431bdbf005b9372c2c174698d2f33914cb5797ab|1658102400000|1668474000000|1673658000000|app/components/gh-brand-settings-form.hbs
add|ember-template-lint|no-invalid-interactive|107|76|107|76|7079e498c9add301f177d9056312b9c5b0848a30|1658102400000|1668474000000|1673658000000|app/components/gh-brand-settings-form.hbs
add|ember-template-lint|no-invalid-interactive|143|83|143|83|25547784a8d412e4463149020bbf91db815057fe|1658102400000|1668474000000|1673658000000|app/components/gh-brand-settings-form.hbs
add|ember-template-lint|require-input-label|22|20|22|20|a9cd381b8ac601bc9cbf7a4eace7bcefa67ecae6|1658102400000|1668474000000|1673658000000|app/components/gh-brand-settings-form.hbs
add|ember-template-lint|require-input-label|40|24|40|24|8aec13864a8e34e86eea56eced36e8271d3bc4ea|1658102400000|1668474000000|1673658000000|app/components/gh-brand-settings-form.hbs
add|ember-template-lint|require-valid-alt-text|107|24|107|24|7079e498c9add301f177d9056312b9c5b0848a30|1658102400000|1668474000000|1673658000000|app/components/gh-brand-settings-form.hbs
add|ember-template-lint|require-valid-alt-text|143|24|143|24|25547784a8d412e4463149020bbf91db815057fe|1658102400000|1668474000000|1673658000000|app/components/gh-brand-settings-form.hbs
add|ember-template-lint|no-action|5|11|5|11|8eaebb48eca1563c6e0b18581df84ab59188d971|1658102400000|1668474000000|1673658000000|app/components/gh-cm-editor.hbs
add|ember-template-lint|no-passed-in-event-handlers|5|4|5|4|3a763e253744b070633bb8bd424b6c8e55f6b20a|1658102400000|1668474000000|1673658000000|app/components/gh-cm-editor.hbs
add|ember-template-lint|no-action|5|18|5|18|7c796afb78a976ab2411eab292c02c4250e429c7|1658102400000|1668474000000|1673658000000|app/components/gh-date-time-picker.hbs
add|ember-template-lint|no-down-event-binding|17|25|17|25|e82f6aa36fd44bb3dccff09770613eee19380f9b|1658102400000|1668474000000|1673658000000|app/components/gh-date-time-picker.hbs
add|ember-template-lint|require-input-label|11|16|11|16|36ecd3af7ac0e4bbafbf933cc20ff9f3ff8f0947|1658102400000|1668474000000|1673658000000|app/components/gh-date-time-picker.hbs
add|ember-template-lint|require-input-label|29|8|29|8|334a3ea0fb16fabf58967294e125af8437504bdb|1658102400000|1668474000000|1673658000000|app/components/gh-date-time-picker.hbs
add|ember-template-lint|no-invalid-interactive|3|4|3|4|66accf6cbc192fd9273063ef67798572309ff1bb|1658102400000|1668474000000|1673658000000|app/components/gh-editor-feature-image.hbs
add|ember-template-lint|require-input-label|48|20|48|20|213bf91395d670b2af5fc667fca46d3d20238465|1658102400000|1668474000000|1673658000000|app/components/gh-editor-feature-image.hbs
add|ember-template-lint|require-valid-alt-text|41|16|41|16|079fc89fa5c7c47f6b0b219820cdda3819c44e26|1658102400000|1668474000000|1673658000000|app/components/gh-editor-feature-image.hbs
add|ember-template-lint|no-action|9|21|9|21|4287fc3d450426e99d8f6b7f06b7e8bf5853ca27|1658102400000|1668474000000|1673658000000|app/components/gh-editor.hbs
add|ember-template-lint|no-action|10|18|10|18|268f66f239be50906625635120971fc67a7fefd7|1658102400000|1668474000000|1673658000000|app/components/gh-editor.hbs
add|ember-template-lint|no-action|11|22|11|22|e3540dcab854d420bb1a1f9dd3435421f97ceac2|1658102400000|1668474000000|1673658000000|app/components/gh-editor.hbs
add|ember-template-lint|no-action|12|19|12|19|d12bcf1144bfb2fe70e7ab0f66836f1c6207a589|1658102400000|1668474000000|1673658000000|app/components/gh-editor.hbs
add|ember-template-lint|no-action|13|20|13|20|16d94650de2ffbe8ee3f2ce3ba5ca97a6304b739|1658102400000|1668474000000|1673658000000|app/components/gh-editor.hbs
add|ember-template-lint|no-action|14|17|14|17|30d64b1bf8990e2f84c52665690739fcc726e9f7|1658102400000|1668474000000|1673658000000|app/components/gh-editor.hbs
add|ember-template-lint|no-action|1|150|1|150|9eb2aa69ff4ec506e54d133fa8c91e6b9953d04c|1658102400000|1668474000000|1673658000000|app/components/gh-feature-flag.hbs
add|ember-template-lint|no-triple-curlies|3|0|3|0|7ceb921960f42e312847d43c601bc98f2eced9f4|1658102400000|1668474000000|1673658000000|app/components/gh-feature-flag.hbs
add|ember-template-lint|no-action|2|111|2|111|7e581bf2ffd5254ae851201e1f23cb9eaa9b198b|1658102400000|1668474000000|1673658000000|app/components/gh-file-upload.hbs
add|ember-template-lint|require-input-label|1|0|1|0|80e39573b35eef3b8f874fbc35bdd2bfd65695b1|1658102400000|1668474000000|1673658000000|app/components/gh-file-upload.hbs
add|ember-template-lint|no-action|6|77|6|77|76726a13a086d82dab219df12e86db1773a9de32|1658102400000|1668474000000|1673658000000|app/components/gh-file-uploader.hbs
add|ember-template-lint|no-action|18|73|18|73|bbc5d9a459cd07e56d8c79dde619775b89d7cc89|1658102400000|1668474000000|1673658000000|app/components/gh-file-uploader.hbs
add|ember-template-lint|no-action|2|45|2|45|a9d29fae15800842d0e7b8f32b035ee22a23f4cb|1658102400000|1668474000000|1673658000000|app/components/gh-fullscreen-modal.hbs
add|ember-template-lint|no-action|10|29|10|29|ebbd89a393bcec7f537f2104ace2a6b1941a19a7|1658102400000|1668474000000|1673658000000|app/components/gh-fullscreen-modal.hbs
add|ember-template-lint|no-action|11|32|11|32|ab89b6f10c519be1271386203e9439d261eecd67|1658102400000|1668474000000|1673658000000|app/components/gh-fullscreen-modal.hbs
add|ember-template-lint|no-action|13|36|13|36|3776877637b49b65deef537c68ae490b2fb081a9|1658102400000|1668474000000|1673658000000|app/components/gh-fullscreen-modal.hbs
add|ember-template-lint|no-invalid-interactive|2|45|2|45|b6693259da29d433264b59abc9a7c7ac846a4bf6|1658102400000|1668474000000|1673658000000|app/components/gh-fullscreen-modal.hbs
add|ember-template-lint|require-iframe-title|6|4|6|4|62bc251294f2cbea568fcc8d8e46bb5fe1915bc9|1658102400000|1668474000000|1673658000000|app/components/gh-html-iframe.hbs
add|ember-template-lint|require-iframe-title|7|4|7|4|62bc251294f2cbea568fcc8d8e46bb5fe1915bc9|1658102400000|1668474000000|1673658000000|app/components/gh-html-iframe.hbs
add|ember-template-lint|link-href-attributes|4|8|4|8|a9119f612d27ee1fc92e9bb2c77b6fd30cab622a|1658102400000|1668474000000|1673658000000|app/components/gh-image-uploader-with-preview.hbs
add|ember-template-lint|no-invalid-interactive|4|47|4|47|a9119f612d27ee1fc92e9bb2c77b6fd30cab622a|1658102400000|1668474000000|1673658000000|app/components/gh-image-uploader-with-preview.hbs
add|ember-template-lint|require-valid-alt-text|3|13|3|13|079fc89fa5c7c47f6b0b219820cdda3819c44e26|1658102400000|1668474000000|1673658000000|app/components/gh-image-uploader-with-preview.hbs
add|ember-template-lint|no-action|12|58|12|58|031d04576149d3034549aa3d14bc26da704cd70c|1658102400000|1668474000000|1673658000000|app/components/gh-image-uploader.hbs
add|ember-template-lint|no-action|17|75|17|75|bbc5d9a459cd07e56d8c79dde619775b89d7cc89|1658102400000|1668474000000|1673658000000|app/components/gh-image-uploader.hbs
add|ember-template-lint|no-action|22|52|22|52|b1bd53a513ad82434d5a0a9a96441a45d416d7ad|1658102400000|1668474000000|1673658000000|app/components/gh-image-uploader.hbs
add|ember-template-lint|no-action|31|16|31|16|4c64ddeaf795ee831d0d7346667a305814ca9855|1658102400000|1668474000000|1673658000000|app/components/gh-image-uploader.hbs
add|ember-template-lint|no-action|32|15|32|15|b1bd53a513ad82434d5a0a9a96441a45d416d7ad|1658102400000|1668474000000|1673658000000|app/components/gh-image-uploader.hbs
add|ember-template-lint|no-invalid-interactive|22|52|22|52|74dea739bef284d6557987ff3da53fa1278030e2|1658102400000|1668474000000|1673658000000|app/components/gh-image-uploader.hbs
add|ember-template-lint|no-down-event-binding|5|13|5|13|a158be60fde14211f6abfbac329c94fa5a3b4a46|1658102400000|1668474000000|1673658000000|app/components/gh-koenig-editor.hbs
add|ember-template-lint|no-invalid-interactive|5|8|5|8|94046126dd697b080aa16222b00a3d5a545ee001|1658102400000|1668474000000|1673658000000|app/components/gh-koenig-editor.hbs
add|ember-template-lint|no-invalid-interactive|6|8|6|8|94046126dd697b080aa16222b00a3d5a545ee001|1658102400000|1668474000000|1673658000000|app/components/gh-koenig-editor.hbs
add|ember-template-lint|no-passed-in-event-handlers|26|12|26|12|4ad1176b9a4bb23d79114735b478240166113502|1658102400000|1668474000000|1673658000000|app/components/gh-koenig-editor.hbs
add|ember-template-lint|no-passed-in-event-handlers|28|12|28|12|7ffe0a55c81efcfcd46880a930fa69bee73835b3|1658102400000|1668474000000|1673658000000|app/components/gh-koenig-editor.hbs
add|ember-template-lint|no-action|6|17|6|17|4958fce0dda72cfdd5d49359244c65d37e804763|1658102400000|1668474000000|1673658000000|app/components/gh-markdown-editor.hbs
add|ember-template-lint|no-action|7|16|7|16|91d44afd854e83acf8ffa4ed94b9c8cbaab5c637|1658102400000|1668474000000|1673658000000|app/components/gh-markdown-editor.hbs
add|ember-template-lint|no-action|8|15|8|15|432c540e6c2d193c6de2590afcdaeca15c8bedbd|1658102400000|1668474000000|1673658000000|app/components/gh-markdown-editor.hbs
add|ember-template-lint|no-action|9|21|9|21|5cae535312c5bb6bd16943db8003819b3cdfc4f6|1658102400000|1668474000000|1673658000000|app/components/gh-markdown-editor.hbs
add|ember-template-lint|no-action|13|10|13|10|cd0da3fe1ab45eeef8705bff73b49199eabeb12c|1658102400000|1668474000000|1673658000000|app/components/gh-markdown-editor.hbs
add|ember-template-lint|no-action|19|16|19|16|770eeb17fc003a2f7a88e53c530684441f1325b3|1658102400000|1668474000000|1673658000000|app/components/gh-markdown-editor.hbs
add|ember-template-lint|no-action|25|16|25|16|c6bbe24d9b26dcec3a60bf7a1f4a8c7879aae806|1658102400000|1668474000000|1673658000000|app/components/gh-markdown-editor.hbs
add|ember-template-lint|no-action|26|15|26|15|342b42844723d115bff978b6ff1327958929a087|1658102400000|1668474000000|1673658000000|app/components/gh-markdown-editor.hbs
add|ember-template-lint|no-action|31|15|31|15|90b03a30c6e96b204b86ce170890fc6849e6f82d|1658102400000|1668474000000|1673658000000|app/components/gh-markdown-editor.hbs
add|ember-template-lint|simple-unless|19|34|19|34|ca177565b6e1c5a6175982047708732f5dde7e59|1658102400000|1668474000000|1673658000000|app/components/gh-member-details-activity.hbs
add|ember-template-lint|simple-unless|18|30|18|30|ca177565b6e1c5a6175982047708732f5dde7e59|1658102400000|1668474000000|1673658000000|app/components/gh-member-details.hbs
add|ember-template-lint|no-action|10|16|10|16|8d8dd8c2cb5f9910c2de5ca6acc76ee4262a876e|1658102400000|1668474000000|1673658000000|app/components/gh-members-import-mapping-input.hbs
add|ember-template-lint|no-action|9|36|9|36|05358b6ec6e9afbaa47416266c49f40a3ffb4490|1658102400000|1668474000000|1673658000000|app/components/gh-members-import-table.hbs
add|ember-template-lint|no-action|10|36|10|36|c5ce93cf577ec47970f715ed83ed11a63adb7a63|1658102400000|1668474000000|1673658000000|app/components/gh-members-import-table.hbs
add|ember-template-lint|no-action|11|35|11|35|6f79b46dcaccf8061b341b369ec20f6e0f0805bd|1658102400000|1668474000000|1673658000000|app/components/gh-members-payments-setting.hbs
add|ember-template-lint|no-action|20|35|20|35|c84840bfbe762e604dc56cf4df06ab19d38a4777|1658102400000|1668474000000|1673658000000|app/components/gh-members-payments-setting.hbs
add|ember-template-lint|no-action|71|78|71|78|09238ca0047aba9705befa8bb94c6d577d7b78cf|1658102400000|1668474000000|1673658000000|app/components/gh-members-payments-setting.hbs
add|ember-template-lint|no-action|82|85|82|85|8d227d609618ed70d583fd1c1ecec3638f03138d|1658102400000|1668474000000|1673658000000|app/components/gh-members-payments-setting.hbs
add|ember-template-lint|no-action|83|123|83|123|d5b4c9ae3e5c4e36db552abdbc83280f9e6f8d0b|1658102400000|1668474000000|1673658000000|app/components/gh-members-payments-setting.hbs
add|ember-template-lint|no-action|93|35|93|35|1808b6250616df5c80cd814986955242d49e70c0|1658102400000|1668474000000|1673658000000|app/components/gh-members-payments-setting.hbs
add|ember-template-lint|no-action|131|17|131|17|79c12d2474c1fd8c0ff5ab8cf1fdb3952ab1825b|1658102400000|1668474000000|1673658000000|app/components/gh-members-payments-setting.hbs
add|ember-template-lint|no-action|132|15|132|15|c7db9d737e3f06cc754d7a49a3e35897117e5777|1658102400000|1668474000000|1673658000000|app/components/gh-members-payments-setting.hbs
add|ember-template-lint|no-passed-in-event-handlers|11|28|11|28|02c81dfb804e41f5b3c10730e431d1e4b0958e6f|1658102400000|1668474000000|1673658000000|app/components/gh-members-payments-setting.hbs
add|ember-template-lint|no-passed-in-event-handlers|20|28|20|28|1591adfb7d0dbab4321126ada2e2c5a4a8c66516|1658102400000|1668474000000|1673658000000|app/components/gh-members-payments-setting.hbs
add|ember-template-lint|no-passed-in-event-handlers|93|28|93|28|55dadf0e7dc5e2ed57771f46ca3cb82607d1799c|1658102400000|1668474000000|1673658000000|app/components/gh-members-payments-setting.hbs
add|ember-template-lint|no-duplicate-attributes|9|4|9|4|6b5f76f812df2b84f2ed9ee5a557ca1bf98710df|1658102400000|1668474000000|1673658000000|app/components/gh-members-segment-select.hbs
add|ember-template-lint|no-action|38|107|38|107|79d2eeaed67e929e989261416abd3cd2dbbf4861|1658102400000|1668474000000|1673658000000|app/components/gh-membership-tiers-alpha.hbs
add|ember-template-lint|no-action|8|50|8|50|de589665046a78748832e8f93d2e495d1d259265|1658102400000|1668474000000|1673658000000|app/components/gh-mobile-nav-bar.hbs
add|ember-template-lint|no-action|20|19|20|19|1cf358e2bf9d6fbb6abdc967c327ac9236e817dc|1658102400000|1668474000000|1673658000000|app/components/gh-navitem.hbs
add|ember-template-lint|no-action|21|22|21|22|b05a3bfe0536cd1cacb3a5803246be6f16857425|1658102400000|1668474000000|1673658000000|app/components/gh-navitem.hbs
add|ember-template-lint|no-action|22|23|22|23|29e3a04f98d37252bdd540ec2adcda840bd6f929|1658102400000|1668474000000|1673658000000|app/components/gh-navitem.hbs
add|ember-template-lint|no-action|38|20|38|20|3c723798059c71aedfa95cd70507bc0308728544|1658102400000|1668474000000|1673658000000|app/components/gh-navitem.hbs
add|ember-template-lint|no-action|39|25|39|25|e27fd2ebfccd2eed3d401b8854fc4d5d2f74cfc2|1658102400000|1668474000000|1673658000000|app/components/gh-navitem.hbs
add|ember-template-lint|no-action|47|49|47|49|18f748b67f9fe570b08c7a1c1d99114f31b41598|1658102400000|1668474000000|1673658000000|app/components/gh-navitem.hbs
add|ember-template-lint|no-action|51|52|51|52|4d50f3b33a672c365969169a4c0ca14cf83fe39c|1658102400000|1668474000000|1673658000000|app/components/gh-navitem.hbs
add|ember-template-lint|no-passed-in-event-handlers|20|12|20|12|4069dec45ac2a31d440780c914b2307ad1c6ea5f|1658102400000|1668474000000|1673658000000|app/components/gh-navitem.hbs
add|ember-template-lint|no-passed-in-event-handlers|21|12|21|12|e53f64794fdd0fe8c8b027d1831942d7c78c503b|1658102400000|1668474000000|1673658000000|app/components/gh-navitem.hbs
add|ember-template-lint|no-action|39|50|39|50|317b0cce151252b8cb54fb112fef62cf3c285ad7|1658102400000|1668474000000|1673658000000|app/components/gh-portal-links.hbs
add|ember-template-lint|no-action|69|50|69|50|0b06c9d99b2e88f3e5c134fa9452f3a3ee107649|1658102400000|1668474000000|1673658000000|app/components/gh-portal-links.hbs
add|ember-template-lint|no-action|96|50|96|50|542d2c922b1ddd938aabf03ce17fe33053749ec7|1658102400000|1668474000000|1673658000000|app/components/gh-portal-links.hbs
add|ember-template-lint|no-action|125|48|125|48|01f579966c325cf66d3a87799e8cea7dd1fded81|1658102400000|1668474000000|1673658000000|app/components/gh-portal-links.hbs
add|ember-template-lint|no-action|148|54|148|54|884d0df74365d794c99bb098e72dc267bd0fbc46|1658102400000|1668474000000|1673658000000|app/components/gh-portal-links.hbs
add|ember-template-lint|no-action|175|54|175|54|b51c80e0c3de828988f74017ab7e383552f48648|1658102400000|1668474000000|1673658000000|app/components/gh-portal-links.hbs
add|ember-template-lint|no-action|202|54|202|54|7f4e772017b88e0a2f39ecf899f0c506639bc675|1658102400000|1668474000000|1673658000000|app/components/gh-portal-links.hbs
add|ember-template-lint|no-action|230|54|230|54|eca87a98b0e8ad5a623de88a16aadedc9591ac22|1658102400000|1668474000000|1673658000000|app/components/gh-portal-links.hbs
add|ember-template-lint|no-action|257|54|257|54|e877a0feccc071327b56a0d7b8a8fcb84ee6d840|1658102400000|1668474000000|1673658000000|app/components/gh-portal-links.hbs
add|ember-template-lint|no-action|284|54|284|54|7f4e772017b88e0a2f39ecf899f0c506639bc675|1658102400000|1668474000000|1673658000000|app/components/gh-portal-links.hbs
add|ember-template-lint|no-action|315|50|315|50|1d7de8deaee7bfe5bd051e900e98817fedabc529|1658102400000|1668474000000|1673658000000|app/components/gh-portal-links.hbs
add|ember-template-lint|no-action|342|50|342|50|df19e5858021b80de54052c953e70e3b4606a8f5|1658102400000|1668474000000|1673658000000|app/components/gh-portal-links.hbs
add|ember-template-lint|no-action|369|50|369|50|3afa41e4d86dd7e5c049a762f0f761c2464a5f96|1658102400000|1668474000000|1673658000000|app/components/gh-portal-links.hbs
add|ember-template-lint|no-action|35|35|35|35|66cebfc8448eced0bccf3faa57b4af0cd633e65e|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-action|36|39|36|39|70f3e9aa9a9aa52142c4e0c015a8f173d12b05ed|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-action|55|33|55|33|43cc094c1a6b50ecd24c8af325dfdfcac863bb14|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-action|56|33|56|33|e7f429eefd04a55d4ef1875fe601da1b69964364|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-action|86|42|86|42|0e827c1770073f988535ceb9d6c49808a153d896|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-action|103|31|103|31|5cbc9d2abf29e108bfc207579df1206485e2a74d|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-action|104|35|104|35|1f2dd4961e9757ede9706bb0aa9b8baf9c49b22b|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-action|114|97|114|97|90dbb84741c72aa86a601e1cb95c066bd53898bd|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-action|121|46|121|46|97acfb2045b33a97621258596b631362ad4c56d5|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-action|128|47|128|47|358336432fcb413c522c5f1ff3062a68ef9f449f|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-action|135|46|135|46|78a45cbb7eafdda134d96f6035b0026f339e75ad|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-action|143|50|143|50|91a3281547c8446529685240c77aaccb5c7d69bd|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-action|151|46|151|46|9f1c3c88187e5db774f97704269e372f8331eba5|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-action|161|59|161|59|b07bf9d50af5f00861571521b150cf6504b90704|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-action|166|36|166|36|38755451c044c56040684339301c21b2aab49ecb|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-action|177|38|177|38|56ca074eb0236b8becc4084423056a8ffa4453e9|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-action|180|114|180|114|de5b68b49193c72f85c0e478f151a00180321d91|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-action|192|163|192|163|0145b67f0faef0aad141c6a4269c35c6ef8f0a47|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-action|198|34|198|34|3d44a2ad21d60d18bed6f79265caa4887361aaa4|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-action|207|47|207|47|67551960e57b2ad06d24289e0d6f256dc5635cae|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-action|208|51|208|51|50e3029f4c845049ff10130cf0dc2ea06c7a3d2d|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-action|223|47|223|47|0d852775b4028d61c2c91b5d821c79de3cdbd1de|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-action|224|51|224|51|194439ec4cb10e176eecb1f55c2995c00f6c67b4|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-action|238|47|238|47|3be93048908ab43d74726c3983982fcb9a3570e3|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-action|239|51|239|51|40871d259cc307345b8096e4215ae58e5886b724|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-action|265|166|265|166|0145b67f0faef0aad141c6a4269c35c6ef8f0a47|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-action|272|34|272|34|3d44a2ad21d60d18bed6f79265caa4887361aaa4|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-action|277|44|277|44|d2abbb4bb55b6cb1131ca0bc56c31632b32b980c|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-action|278|44|278|44|3ede83bd64d206d665edb4f1b73f03b4122edfad|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-action|288|47|288|47|13b7064b520a924d9e57a4a680621ce979d23923|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-action|289|51|289|51|b0fc38b818f4ca2613684588e4122e12da0090b9|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-action|304|47|304|47|f34f084dc72c079be9ba3eb93c255bd8853612bd|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-action|305|51|305|51|c5c09d001fa96a623649cc88ef13b3c6c164f05b|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-action|335|35|335|35|0145b67f0faef0aad141c6a4269c35c6ef8f0a47|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-action|341|167|341|167|0145b67f0faef0aad141c6a4269c35c6ef8f0a47|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-action|347|34|347|34|3d44a2ad21d60d18bed6f79265caa4887361aaa4|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-action|352|44|352|44|32101c0834d9e8d2caa87dee9cb1a92fe633cfde|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-action|353|44|353|44|c8a82286cfe4220cb2d8a059b1daf7f6c8d54cf5|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-action|363|47|363|47|31800fb83f1797de5d91b7007c48432cf2fae803|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-action|364|51|364|51|5ce0788874489341385f031810e9f7f62724aaf8|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-action|377|47|377|47|1cbd7d922a9d202772a9dca214d97bed18777d6e|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-action|378|51|378|51|16fb2c7c87dc282c6ee49a032aaaf22628b88084|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-action|409|168|409|168|0145b67f0faef0aad141c6a4269c35c6ef8f0a47|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-action|415|34|415|34|3d44a2ad21d60d18bed6f79265caa4887361aaa4|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-action|422|50|422|50|d03e7bbf2b6de94b08fae1c33bd5a2f16874e03a|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-action|424|48|424|48|bca167f0250b40d56ff1ed40ec21bc88f8e27ec3|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-action|435|50|435|50|93ae447fb1054de4dbe501fc60167a6ff3273687|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-action|437|48|437|48|ecb9ed2577be7ea0300563f22d8e9fde2fed12a6|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-passed-in-event-handlers|35|28|35|28|fed655605208b290a18b9f7da51a6cf7b40c0e9a|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-passed-in-event-handlers|103|24|103|24|187e85e14470b453a1e6df8c5f18549d11c441a0|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-passed-in-event-handlers|207|40|207|40|f290a04fd56f613d80d61244d161631f630185a8|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-passed-in-event-handlers|223|40|223|40|c1af88109f705acc93fab4ee7b4d89096136ffe1|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-passed-in-event-handlers|238|40|238|40|852cbb2c63e19a28d700d3b18e6f887edef303fa|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-passed-in-event-handlers|288|40|288|40|58185c92e8b6261ea1483a70296d9fa837d3f2f5|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-passed-in-event-handlers|304|40|304|40|c4353acd715c396564c69389edd0a52246d3b966|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-passed-in-event-handlers|363|40|363|40|29f26e46559dc40d9724a05b7516cb52f1481aaa|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-passed-in-event-handlers|377|40|377|40|42e0617f832585eaa056c9c66dffe1a126318faf|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-passed-in-event-handlers|422|40|422|40|d1dccfeee5b103fac3b14d5b4567bc65ee08fd5a|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-passed-in-event-handlers|435|40|435|40|055c4b70aa8daba6b97077eefa95ebed7f7f5315|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|simple-unless|64|30|64|30|ca177565b6e1c5a6175982047708732f5dde7e59|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|simple-unless|188|62|188|62|5d9203e10703908836b537481cf012f167ded239|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu.hbs
add|ember-template-lint|no-action|4|14|4|14|c8f6146f9286ec1b289e28983ab446386f390f01|1658102400000|1668474000000|1673658000000|app/components/gh-psm-authors-input.hbs
add|ember-template-lint|no-action|5|14|5|14|a90edd9a99596008f60bfcdbc6befe7fe8d26321|1658102400000|1668474000000|1673658000000|app/components/gh-psm-tags-input.hbs
add|ember-template-lint|no-action|6|14|6|14|3924d7cfb394cbcfd6b1bf9cf13a5040ac8f94b5|1658102400000|1668474000000|1673658000000|app/components/gh-psm-tags-input.hbs
add|ember-template-lint|no-action|10|20|10|20|8b7921c0514cfd3fec8c3a474187048bc26faf7f|1658102400000|1668474000000|1673658000000|app/components/gh-psm-tags-input.hbs
add|ember-template-lint|no-action|11|28|11|28|2e612be5e2b8e0b792f951c3c75b2f71df880365|1658102400000|1668474000000|1673658000000|app/components/gh-psm-template-select.hbs
add|ember-template-lint|no-action|7|16|7|16|86819b20fcc42bc1d4607519c1d45532ee337884|1658102400000|1668474000000|1673658000000|app/components/gh-psm-visibility-input.hbs
add|ember-template-lint|no-invalid-interactive|8|80|8|80|bb724c391e9e56cc994322717d453c488ca5ae28|1658102400000|1668474000000|1673658000000|app/components/gh-role-selection.hbs
add|ember-template-lint|no-invalid-interactive|36|75|36|75|7eff29b73ce875c9b087ff1340647bab0d21ac19|1658102400000|1668474000000|1673658000000|app/components/gh-role-selection.hbs
add|ember-template-lint|no-invalid-interactive|72|75|72|75|d541b87fe28c3dc1c1fce306c988bb09f777b1bc|1658102400000|1668474000000|1673658000000|app/components/gh-role-selection.hbs
add|ember-template-lint|no-invalid-interactive|108|82|108|82|f77e549a4fa7232cea39feff73f3e1c5bc98f467|1658102400000|1668474000000|1673658000000|app/components/gh-role-selection.hbs
add|ember-template-lint|no-autofocus-attribute|13|12|13|12|fa0ffb960072633b72117849e3927673be0059af|1658102400000|1668474000000|1673658000000|app/components/gh-search-input.hbs
add|ember-template-lint|no-yield-only|1|0|1|0|a5fa6e8c1e0f03fb31b5cd17770e4368d44932e4|1658102400000|1668474000000|1673658000000|app/components/gh-simplemde.hbs
add|ember-template-lint|require-iframe-title|1|0|1|0|956ab219134ac63aec3fd2d35582076c21631b75|1658102400000|1668474000000|1673658000000|app/components/gh-site-iframe.hbs
add|ember-template-lint|no-action|14|39|14|39|913b0cf6525ac677112a899b2b0f15e07fbf204a|1658102400000|1668474000000|1673658000000|app/components/gh-tag-settings-form.hbs
add|ember-template-lint|no-action|65|35|65|35|39b616c69eeb0dc420dc1430e0ecd38668f4d3fa|1658102400000|1668474000000|1673658000000|app/components/gh-tag-settings-form.hbs
add|ember-template-lint|no-action|79|35|79|35|9b8af5e8622a324a4f80d12a88e2af649e742fe6|1658102400000|1668474000000|1673658000000|app/components/gh-tag-settings-form.hbs
add|ember-template-lint|no-action|91|28|91|28|0b3589b667ef29b9e01bb49bd2645db31eceed1a|1658102400000|1668474000000|1673658000000|app/components/gh-tag-settings-form.hbs
add|ember-template-lint|no-action|92|28|92|28|be80faed6c7a41a2ac377b4f6d3eece8b858408c|1658102400000|1668474000000|1673658000000|app/components/gh-tag-settings-form.hbs
add|ember-template-lint|no-action|106|63|106|63|3cbb83b28eaf63d37b15aa99151bac2a301a4b41|1658102400000|1668474000000|1673658000000|app/components/gh-tag-settings-form.hbs
add|ember-template-lint|no-action|122|43|122|43|4a1f665e2bd695f7db25f2a126670a524a3a112b|1658102400000|1668474000000|1673658000000|app/components/gh-tag-settings-form.hbs
add|ember-template-lint|no-action|137|43|137|43|763dced49c969df89ec39d7258b70c1e91c74c67|1658102400000|1668474000000|1673658000000|app/components/gh-tag-settings-form.hbs
add|ember-template-lint|no-action|149|43|149|43|b0ec7092966648e6ba28055946b1fff735f8c08d|1658102400000|1668474000000|1673658000000|app/components/gh-tag-settings-form.hbs
add|ember-template-lint|no-action|180|63|180|63|afe185fdf8e3368a25e965330d4fca4da08b63a7|1658102400000|1668474000000|1673658000000|app/components/gh-tag-settings-form.hbs
add|ember-template-lint|no-action|194|40|194|40|d2abbb4bb55b6cb1131ca0bc56c31632b32b980c|1658102400000|1668474000000|1673658000000|app/components/gh-tag-settings-form.hbs
add|ember-template-lint|no-action|195|40|195|40|3ede83bd64d206d665edb4f1b73f03b4122edfad|1658102400000|1668474000000|1673658000000|app/components/gh-tag-settings-form.hbs
add|ember-template-lint|no-action|206|43|206|43|c742c00638da1ba8d067d21307f96e57df44c2b7|1658102400000|1668474000000|1673658000000|app/components/gh-tag-settings-form.hbs
add|ember-template-lint|no-action|221|43|221|43|bd00aa5009b99f82b02b9ea6ddf65ba8fa74d1ad|1658102400000|1668474000000|1673658000000|app/components/gh-tag-settings-form.hbs
add|ember-template-lint|no-action|274|63|274|63|4842445c912ca9073389537718b6fe02c3e621e3|1658102400000|1668474000000|1673658000000|app/components/gh-tag-settings-form.hbs
add|ember-template-lint|no-action|288|40|288|40|32101c0834d9e8d2caa87dee9cb1a92fe633cfde|1658102400000|1668474000000|1673658000000|app/components/gh-tag-settings-form.hbs
add|ember-template-lint|no-action|289|40|289|40|c8a82286cfe4220cb2d8a059b1daf7f6c8d54cf5|1658102400000|1668474000000|1673658000000|app/components/gh-tag-settings-form.hbs
add|ember-template-lint|no-action|300|43|300|43|fd3e947561e280e8a04867bfb9143df32dec0aa9|1658102400000|1668474000000|1673658000000|app/components/gh-tag-settings-form.hbs
add|ember-template-lint|no-action|315|43|315|43|b8cf39b2313c6eaa9d4e9042eaf133079b6a5743|1658102400000|1668474000000|1673658000000|app/components/gh-tag-settings-form.hbs
add|ember-template-lint|no-action|369|63|369|63|fad67fa01283f9b309c508725105158c3243b324|1658102400000|1668474000000|1673658000000|app/components/gh-tag-settings-form.hbs
add|ember-template-lint|no-action|381|34|381|34|74ae86c6b029ad523f821e7385eeb58bc2ab4975|1658102400000|1668474000000|1673658000000|app/components/gh-tag-settings-form.hbs
add|ember-template-lint|no-action|383|32|383|32|cc59d0154028a8f107aaaea52ea6d9e8710b3f83|1658102400000|1668474000000|1673658000000|app/components/gh-tag-settings-form.hbs
add|ember-template-lint|no-action|394|34|394|34|406ce271cdc31541e8199b7729098b4392dc133e|1658102400000|1668474000000|1673658000000|app/components/gh-tag-settings-form.hbs
add|ember-template-lint|no-action|396|32|396|32|2b3c6732bdd27bdef2d535007c670f1f4286db0e|1658102400000|1668474000000|1673658000000|app/components/gh-tag-settings-form.hbs
add|ember-template-lint|no-passed-in-event-handlers|381|24|381|24|d8b5d7c4140d1dc6151eb7486382da38928232b0|1658102400000|1668474000000|1673658000000|app/components/gh-tag-settings-form.hbs
add|ember-template-lint|no-passed-in-event-handlers|394|24|394|24|3e1dad0fb19c62adea14c5534d52cd95cfad280a|1658102400000|1668474000000|1673658000000|app/components/gh-tag-settings-form.hbs
add|ember-template-lint|require-input-label|29|28|29|28|af62541280c3dc86d2ca7a4be8c980ee164fba5a|1658102400000|1668474000000|1673658000000|app/components/gh-tag-settings-form.hbs
add|ember-template-lint|require-input-label|46|32|46|32|8e299cf5bf0e93e054410239451997a196933d25|1658102400000|1668474000000|1673658000000|app/components/gh-tag-settings-form.hbs
add|ember-template-lint|no-yield-only|1|0|1|0|a5fa6e8c1e0f03fb31b5cd17770e4368d44932e4|1658102400000|1668474000000|1673658000000|app/components/gh-text-input.hbs
add|ember-template-lint|no-action|1|51|1|51|674a942db62e014b25156b02d014f6c63b2f1cb2|1658102400000|1668474000000|1673658000000|app/components/gh-theme-error-li.hbs
add|ember-template-lint|no-triple-curlies|5|12|5|12|5ccce366ce8389f55dc57b1d684c310e65eb26a6|1658102400000|1668474000000|1673658000000|app/components/gh-theme-error-li.hbs
add|ember-template-lint|no-triple-curlies|19|8|19|8|87b1676e19127a8136984abf3f6b3947569afc25|1658102400000|1668474000000|1673658000000|app/components/gh-theme-error-li.hbs
add|ember-template-lint|no-unused-block-params|2|48|2|48|93a49e8b39e3073babdfb7d6b9f8d061d712564d|1658102400000|1668474000000|1673658000000|app/components/gh-theme-table.hbs
add|ember-template-lint|no-action|12|16|12|16|40e7337ccf8501ef3b2fea94812d4de8435286da|1658102400000|1668474000000|1673658000000|app/components/gh-tiers-price-billingperiod.hbs
add|ember-template-lint|no-action|9|16|9|16|0232e593042bb48fcc89bf9ab0b91b242f264fba|1658102400000|1668474000000|1673658000000|app/components/gh-timezone-select.hbs
add|ember-template-lint|no-action|2|11|2|11|2b1317e72b94ec31bf2700acc3f041e6be974b72|1658102400000|1668474000000|1673658000000|app/components/gh-uploader.hbs
add|ember-template-lint|no-action|7|13|7|13|add4b57d48167f809045245535d45bedb00cb753|1658102400000|1668474000000|1673658000000|app/components/gh-uploader.hbs
add|ember-template-lint|no-action|8|22|8|22|ce1eba2b791c0f120baf10871fd3949c1b9f6a79|1658102400000|1668474000000|1673658000000|app/components/gh-uploader.hbs
add|ember-template-lint|no-action|9|22|9|22|0209f233628ec084b87894b4be9073c29f2a4f47|1658102400000|1668474000000|1673658000000|app/components/gh-uploader.hbs
add|ember-template-lint|no-passed-in-event-handlers|4|4|4|4|3dedc53191768d81765eac4a7a049a9aba7df442|1658102400000|1668474000000|1673658000000|app/components/gh-url-input.hbs
add|ember-template-lint|no-action|1|71|1|71|2e6351f546807d88cc8eb9dbe8baa149468b5cb9|1658102400000|1668474000000|1673658000000|app/components/gh-view-title.hbs
add|ember-template-lint|no-invalid-role|1|0|1|0|3e651d38e0110e1be20e5082075db1b879b59a36|1658102400000|1668474000000|1673658000000|app/components/gh-view-title.hbs
add|ember-template-lint|no-action|4|53|4|53|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-delete-all.hbs
add|ember-template-lint|no-action|11|41|11|41|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-delete-all.hbs
add|ember-template-lint|no-action|4|53|4|53|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-delete-integration.hbs
add|ember-template-lint|no-action|11|41|11|41|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-delete-integration.hbs
add|ember-template-lint|no-action|4|53|4|53|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-delete-member.hbs
add|ember-template-lint|no-action|19|37|19|37|56d2776c5303897b2f18863de273e2534e988a86|1658102400000|1668474000000|1673658000000|app/components/modal-delete-member.hbs
add|ember-template-lint|no-action|33|41|33|41|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-delete-member.hbs
add|ember-template-lint|no-action|4|53|4|53|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-delete-snippet.hbs
add|ember-template-lint|no-action|13|41|13|41|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-delete-snippet.hbs
add|ember-template-lint|no-action|4|53|4|53|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-delete-tag.hbs
add|ember-template-lint|no-action|14|41|14|41|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-delete-tag.hbs
add|ember-template-lint|no-action|4|53|4|53|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-delete-user.hbs
add|ember-template-lint|no-action|23|79|23|79|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-delete-user.hbs
add|ember-template-lint|no-action|4|53|4|53|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-delete-webhook.hbs
add|ember-template-lint|no-action|13|41|13|41|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-delete-webhook.hbs
add|ember-template-lint|no-action|4|53|4|53|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-disconnect-stripe.hbs
add|ember-template-lint|no-action|15|41|15|41|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-disconnect-stripe.hbs
add|ember-template-lint|no-action|4|53|4|53|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-early-access.hbs
add|ember-template-lint|no-action|11|41|11|41|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-early-access.hbs
add|ember-template-lint|no-action|12|56|12|56|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-early-access.hbs
add|ember-template-lint|no-action|6|62|6|62|7234f2e9e2cd7dc3a2532ce2003ab3bf7b79f479|1658102400000|1668474000000|1673658000000|app/components/modal-email-design-settings.hbs
add|ember-template-lint|no-down-event-binding|8|21|8|21|a158be60fde14211f6abfbac329c94fa5a3b4a46|1658102400000|1668474000000|1673658000000|app/components/modal-email-design-settings.hbs
add|ember-template-lint|no-triple-curlies|235|32|235|32|f62f2356fb4ed0c7b896b8a97921586b5665f812|1658102400000|1668474000000|1673658000000|app/components/modal-email-design-settings.hbs
add|ember-template-lint|require-valid-alt-text|46|44|46|44|23f4d5a694823e4caeefe34f28ae1a0f56604d27|1658102400000|1668474000000|1673658000000|app/components/modal-email-design-settings.hbs
add|ember-template-lint|require-valid-alt-text|205|32|205|32|97fa01f3f8879263268614e635b5c0f5bb66fa30|1658102400000|1668474000000|1673658000000|app/components/modal-email-design-settings.hbs
add|ember-template-lint|require-valid-alt-text|211|36|211|36|902a4532f0b313f8b929d779f0f7c9058549f850|1658102400000|1668474000000|1673658000000|app/components/modal-email-design-settings.hbs
add|ember-template-lint|no-action|4|55|4|55|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-free-membership-settings.hbs
add|ember-template-lint|no-action|4|79|4|79|d465b362b15b90cf42a093e72895155f49cdf6f2|1658102400000|1668474000000|1673658000000|app/components/modal-free-membership-settings.hbs
add|ember-template-lint|no-action|16|27|16|27|d0065f512527491cf1e2532437e84b9cf867c925|1658102400000|1668474000000|1673658000000|app/components/modal-free-membership-settings.hbs
add|ember-template-lint|no-action|26|27|26|27|b5a468ed19153bfd25303ca31bc45aeaf65c358f|1658102400000|1668474000000|1673658000000|app/components/modal-free-membership-settings.hbs
add|ember-template-lint|no-action|37|31|37|31|085289ac1e1263eed8737dc26c8774a9ea90598b|1658102400000|1668474000000|1673658000000|app/components/modal-free-membership-settings.hbs
add|ember-template-lint|no-action|38|33|38|33|bf202ad54f2ff2a7e57d2e657d4d68f7814eba84|1658102400000|1668474000000|1673658000000|app/components/modal-free-membership-settings.hbs
add|ember-template-lint|no-action|55|71|55|71|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-free-membership-settings.hbs
add|ember-template-lint|no-down-event-binding|4|112|4|112|b3e87879e52edc06bb07f1ad0becc6ec762bbb39|1658102400000|1668474000000|1673658000000|app/components/modal-free-membership-settings.hbs
add|ember-template-lint|no-passed-in-event-handlers|16|20|16|20|5fc3de620138bb4bee07640a9114b5e2be172f59|1658102400000|1668474000000|1673658000000|app/components/modal-free-membership-settings.hbs
add|ember-template-lint|no-passed-in-event-handlers|26|20|26|20|8a899292e274791646c6c820fc584ac3aa4b1d83|1658102400000|1668474000000|1673658000000|app/components/modal-free-membership-settings.hbs
add|ember-template-lint|no-action|5|50|5|50|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-impersonate-member.hbs
add|ember-template-lint|no-action|5|74|5|74|d465b362b15b90cf42a093e72895155f49cdf6f2|1658102400000|1668474000000|1673658000000|app/components/modal-impersonate-member.hbs
add|ember-template-lint|no-down-event-binding|5|107|5|107|b3e87879e52edc06bb07f1ad0becc6ec762bbb39|1658102400000|1668474000000|1673658000000|app/components/modal-impersonate-member.hbs
add|ember-template-lint|no-action|44|57|44|57|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-import-members.hbs
add|ember-template-lint|no-action|51|56|51|56|190532b9954beb4e7e9e0f1c51d170e64d4a5315|1658102400000|1668474000000|1673658000000|app/components/modal-import-members.hbs
add|ember-template-lint|no-action|57|34|57|34|ff609af61e9159dfc5386543d559f6d217d39531|1658102400000|1668474000000|1673658000000|app/components/modal-import-members.hbs
add|ember-template-lint|no-action|58|29|58|29|9a0a72738b6e1a4f46415b2328c37d1814562717|1658102400000|1668474000000|1673658000000|app/components/modal-import-members.hbs
add|ember-template-lint|no-action|111|89|111|89|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-import-members.hbs
add|ember-template-lint|no-action|117|91|117|91|031d04576149d3034549aa3d14bc26da704cd70c|1658102400000|1668474000000|1673658000000|app/components/modal-import-members.hbs
add|ember-template-lint|no-action|120|116|120|116|7e581bf2ffd5254ae851201e1f23cb9eaa9b198b|1658102400000|1668474000000|1673658000000|app/components/modal-import-members.hbs
add|ember-template-lint|no-action|130|120|130|120|031d04576149d3034549aa3d14bc26da704cd70c|1658102400000|1668474000000|1673658000000|app/components/modal-import-members.hbs
add|ember-template-lint|no-action|133|103|133|103|7e581bf2ffd5254ae851201e1f23cb9eaa9b198b|1658102400000|1668474000000|1673658000000|app/components/modal-import-members.hbs
add|ember-template-lint|no-action|144|112|144|112|031d04576149d3034549aa3d14bc26da704cd70c|1658102400000|1668474000000|1673658000000|app/components/modal-import-members.hbs
add|ember-template-lint|no-action|148|110|148|110|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-import-members.hbs
add|ember-template-lint|no-action|154|97|154|97|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-import-members.hbs
add|ember-template-lint|no-action|157|112|157|112|031d04576149d3034549aa3d14bc26da704cd70c|1658102400000|1668474000000|1673658000000|app/components/modal-import-members.hbs
add|ember-template-lint|no-action|161|99|161|99|031d04576149d3034549aa3d14bc26da704cd70c|1658102400000|1668474000000|1673658000000|app/components/modal-import-members.hbs
add|ember-template-lint|no-action|164|110|164|110|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-import-members.hbs
add|ember-template-lint|no-action|172|91|172|91|031d04576149d3034549aa3d14bc26da704cd70c|1658102400000|1668474000000|1673658000000|app/components/modal-import-members.hbs
add|ember-template-lint|no-action|175|102|175|102|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-import-members.hbs
add|ember-template-lint|no-action|182|95|182|95|031d04576149d3034549aa3d14bc26da704cd70c|1658102400000|1668474000000|1673658000000|app/components/modal-import-members.hbs
add|ember-template-lint|no-action|186|102|186|102|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-import-members.hbs
add|ember-template-lint|simple-unless|84|30|84|30|5d9203e10703908836b537481cf012f167ded239|1658102400000|1668474000000|1673658000000|app/components/modal-import-members.hbs
add|ember-template-lint|no-action|2|50|2|50|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-invite-new-user.hbs
add|ember-template-lint|no-action|2|74|2|74|d465b362b15b90cf42a093e72895155f49cdf6f2|1658102400000|1668474000000|1673658000000|app/components/modal-invite-new-user.hbs
add|ember-template-lint|no-action|26|27|26|27|1709109776bf3fc47aaecc21e6d3ec8a0489ad6b|1658102400000|1668474000000|1673658000000|app/components/modal-invite-new-user.hbs
add|ember-template-lint|no-action|28|30|28|30|60f8b21893815d9e7a53a2fd75d89d5eb02516d2|1658102400000|1668474000000|1673658000000|app/components/modal-invite-new-user.hbs
add|ember-template-lint|no-action|37|37|37|37|67e49e01cecb20912daabd71037b7bba8ae35d53|1658102400000|1668474000000|1673658000000|app/components/modal-invite-new-user.hbs
add|ember-template-lint|no-action|38|37|38|37|e9ede6075ef60dc141631856eb197cfc4458ad81|1658102400000|1668474000000|1673658000000|app/components/modal-invite-new-user.hbs
add|ember-template-lint|no-down-event-binding|2|107|2|107|b3e87879e52edc06bb07f1ad0becc6ec762bbb39|1658102400000|1668474000000|1673658000000|app/components/modal-invite-new-user.hbs
add|ember-template-lint|no-passed-in-event-handlers|26|20|26|20|ce0d7e2e732b22ce3643fb9b1ac6ecef07c275ff|1658102400000|1668474000000|1673658000000|app/components/modal-invite-new-user.hbs
add|ember-template-lint|no-action|4|53|4|53|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-leave-settings.hbs
add|ember-template-lint|no-action|15|63|15|63|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-leave-settings.hbs
add|ember-template-lint|no-action|16|75|16|75|ebbd89a393bcec7f537f2104ace2a6b1941a19a7|1658102400000|1668474000000|1673658000000|app/components/modal-leave-settings.hbs
add|ember-template-lint|no-action|4|53|4|53|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-markdown-help.hbs
add|ember-template-lint|no-whitespace-within-word|80|20|80|20|5ec988f82d06bb76be61f23dbb9cb65f94326f34|1658102400000|1668474000000|1673658000000|app/components/modal-markdown-help.hbs
add|ember-template-lint|no-invalid-interactive|28|24|28|24|42a29ae16e22270f0590c9ce5caa7bfec541ca0b|1658102400000|1668474000000|1673658000000|app/components/modal-member-tier.hbs
add|ember-template-lint|no-action|5|57|5|57|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-members-label-form.hbs
add|ember-template-lint|no-action|14|45|14|45|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-members-label-form.hbs
add|ember-template-lint|no-action|23|59|23|59|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-members-label-form.hbs
add|ember-template-lint|no-action|23|83|23|83|d465b362b15b90cf42a093e72895155f49cdf6f2|1658102400000|1668474000000|1673658000000|app/components/modal-members-label-form.hbs
add|ember-template-lint|no-action|34|31|34|31|a8ad062b8379233b7970fe5ea74296fdf5011567|1658102400000|1668474000000|1673658000000|app/components/modal-members-label-form.hbs
add|ember-template-lint|no-action|47|82|47|82|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-members-label-form.hbs
add|ember-template-lint|no-action|49|16|49|16|d465b362b15b90cf42a093e72895155f49cdf6f2|1658102400000|1668474000000|1673658000000|app/components/modal-members-label-form.hbs
add|ember-template-lint|no-action|55|113|55|113|c259009ff744c2e9f7bb273e0eb3a3879036ba85|1658102400000|1668474000000|1673658000000|app/components/modal-members-label-form.hbs
add|ember-template-lint|no-down-event-binding|23|116|23|116|b3e87879e52edc06bb07f1ad0becc6ec762bbb39|1658102400000|1668474000000|1673658000000|app/components/modal-members-label-form.hbs
add|ember-template-lint|no-down-event-binding|49|49|49|49|b3e87879e52edc06bb07f1ad0becc6ec762bbb39|1658102400000|1668474000000|1673658000000|app/components/modal-members-label-form.hbs
add|ember-template-lint|no-down-event-binding|56|21|56|21|a158be60fde14211f6abfbac329c94fa5a3b4a46|1658102400000|1668474000000|1673658000000|app/components/modal-members-label-form.hbs
add|ember-template-lint|no-duplicate-landmark-elements|19|4|19|4|75d318fd9d711332a80f28848acc874ec14b0f4f|1658102400000|1668474000000|1673658000000|app/components/modal-members-label-form.hbs
add|ember-template-lint|no-action|13|112|13|112|decaf649532f1148f5071c2070b99d4e28986150|1658102400000|1668474000000|1673658000000|app/components/modal-portal-settings.hbs
add|ember-template-lint|no-action|18|160|18|160|d7344cff0074353f10626fbe36707360e8ffd52c|1658102400000|1668474000000|1673658000000|app/components/modal-portal-settings.hbs
add|ember-template-lint|no-action|20|150|20|150|d6149e8bd18677704c261b7d3e9afeaf8be9f6a6|1658102400000|1668474000000|1673658000000|app/components/modal-portal-settings.hbs
add|ember-template-lint|no-action|38|69|38|69|b5e82dc8693610f3eb52006adac6456b86fb5ff1|1658102400000|1668474000000|1673658000000|app/components/modal-portal-settings.hbs
add|ember-template-lint|no-action|65|69|65|69|fc5db5638fa960219dcaac666df85bd49ae5ba39|1658102400000|1668474000000|1673658000000|app/components/modal-portal-settings.hbs
add|ember-template-lint|no-action|87|73|87|73|437199817818c66e4539bbc8501bd73148b521fd|1658102400000|1668474000000|1673658000000|app/components/modal-portal-settings.hbs
add|ember-template-lint|no-action|115|73|115|73|4a9c1575e2a51a19d342f234cc41b045ea419ae3|1658102400000|1668474000000|1673658000000|app/components/modal-portal-settings.hbs
add|ember-template-lint|no-action|136|73|136|73|40f0dc3732c554bb3f69d3d126263eb0a24f7738|1658102400000|1668474000000|1673658000000|app/components/modal-portal-settings.hbs
add|ember-template-lint|no-action|150|112|150|112|d4947b819970cbee74170cd33c3067f75dde86d2|1658102400000|1668474000000|1673658000000|app/components/modal-portal-settings.hbs
add|ember-template-lint|no-action|155|85|155|85|d7344cff0074353f10626fbe36707360e8ffd52c|1658102400000|1668474000000|1673658000000|app/components/modal-portal-settings.hbs
add|ember-template-lint|no-action|169|64|169|64|6cb8968d67fbe25c46f1f963ad4f5564f51746f0|1658102400000|1668474000000|1673658000000|app/components/modal-portal-settings.hbs
add|ember-template-lint|no-action|192|64|192|64|4432d73cb43005a6c5ab22362c8773f0f21d0ac4|1658102400000|1668474000000|1673658000000|app/components/modal-portal-settings.hbs
add|ember-template-lint|no-action|203|68|203|68|3ae31e5d2f432b152768be816a2fc40803434382|1658102400000|1668474000000|1673658000000|app/components/modal-portal-settings.hbs
add|ember-template-lint|no-action|210|183|210|183|b84f11a781130328997f76bd3ef2f3faa57a31d0|1658102400000|1668474000000|1673658000000|app/components/modal-portal-settings.hbs
add|ember-template-lint|no-action|224|80|224|80|7fab1dccd9502d972b0b6f3fbbea8340483ada89|1658102400000|1668474000000|1673658000000|app/components/modal-portal-settings.hbs
add|ember-template-lint|no-action|229|150|229|150|22c6e583ac81ee6f218414ee5f5271469891c114|1658102400000|1668474000000|1673658000000|app/components/modal-portal-settings.hbs
add|ember-template-lint|no-action|234|141|234|141|69e5f19c951bf70e9e7eaa72ea2b784ab97c3dff|1658102400000|1668474000000|1673658000000|app/components/modal-portal-settings.hbs
add|ember-template-lint|no-action|261|59|261|59|bb06a1edb1abab80686af224b1e77ff4cba9d604|1658102400000|1668474000000|1673658000000|app/components/modal-portal-settings.hbs
add|ember-template-lint|no-action|272|112|272|112|044d2c50ef7e4765d1130fb40d620b90bae19174|1658102400000|1668474000000|1673658000000|app/components/modal-portal-settings.hbs
add|ember-template-lint|no-action|277|85|277|85|d0601ba765735657d724123264ad81ccb959cd6b|1658102400000|1668474000000|1673658000000|app/components/modal-portal-settings.hbs
add|ember-template-lint|no-action|283|55|283|55|b7fa34dcfcdeab84175766fab6106fe6659f53e7|1658102400000|1668474000000|1673658000000|app/components/modal-portal-settings.hbs
add|ember-template-lint|no-action|306|32|306|32|b7ca157f62b1295d8f5839c8f5d33a6020bc98a8|1658102400000|1668474000000|1673658000000|app/components/modal-portal-settings.hbs
add|ember-template-lint|no-action|313|57|313|57|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-portal-settings.hbs
add|ember-template-lint|no-down-event-binding|315|29|315|29|a158be60fde14211f6abfbac329c94fa5a3b4a46|1658102400000|1668474000000|1673658000000|app/components/modal-portal-settings.hbs
add|ember-template-lint|no-passed-in-event-handlers|261|52|261|52|825f0e3d4a33b20b5154b1fb4905e9ef5b1944d9|1658102400000|1668474000000|1673658000000|app/components/modal-portal-settings.hbs
add|ember-template-lint|no-passed-in-event-handlers|283|48|283|48|f182f9c53280940bba6c3153bd676cce9f4b09b5|1658102400000|1668474000000|1673658000000|app/components/modal-portal-settings.hbs
add|ember-template-lint|no-action|4|53|4|53|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-re-authenticate.hbs
add|ember-template-lint|no-action|8|78|8|78|e65375f48d69b9bad0b967445b0f204783f7352a|1658102400000|1668474000000|1673658000000|app/components/modal-re-authenticate.hbs
add|ember-template-lint|no-action|16|23|16|23|6756e119daad4aa143724e9b56a6aef744d65251|1658102400000|1668474000000|1673658000000|app/components/modal-re-authenticate.hbs
add|ember-template-lint|no-passed-in-event-handlers|16|16|16|16|80681fcec2258c3d81a231bbd635aa1f03ff1452|1658102400000|1668474000000|1673658000000|app/components/modal-re-authenticate.hbs
add|ember-template-lint|no-action|4|53|4|53|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-regenerate-key.hbs
add|ember-template-lint|no-action|20|41|20|41|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-regenerate-key.hbs
add|ember-template-lint|no-autofocus-attribute|16|27|16|27|0d2b98a9093686a223ccc82022c3db762739720f|1658102400000|1668474000000|1673658000000|app/components/modal-regenerate-token.hbs
add|ember-template-lint|no-action|4|53|4|53|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-regenerate-token.hbs
add|ember-template-lint|no-action|16|51|16|51|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-regenerate-token.hbs
add|ember-template-lint|no-action|17|64|17|64|ebbd89a393bcec7f537f2104ace2a6b1941a19a7|1658102400000|1668474000000|1673658000000|app/components/modal-regenerate-token.hbs
add|ember-template-lint|no-action|4|53|4|53|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-reset-all-passwords.hbs
add|ember-template-lint|no-action|11|79|11|79|e36addc82ad99ca2e46fce288afd43c8a56cc904|1658102400000|1668474000000|1673658000000|app/components/modal-reset-all-passwords.hbs
add|ember-template-lint|no-action|23|41|23|41|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-reset-all-passwords.hbs
add|ember-template-lint|no-action|4|55|4|55|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-stripe-connect.hbs
add|ember-template-lint|no-action|4|79|4|79|d465b362b15b90cf42a093e72895155f49cdf6f2|1658102400000|1668474000000|1673658000000|app/components/modal-stripe-connect.hbs
add|ember-template-lint|no-action|21|91|21|91|ebbd89a393bcec7f537f2104ace2a6b1941a19a7|1658102400000|1668474000000|1673658000000|app/components/modal-stripe-connect.hbs
add|ember-template-lint|no-action|22|12|22|12|d465b362b15b90cf42a093e72895155f49cdf6f2|1658102400000|1668474000000|1673658000000|app/components/modal-stripe-connect.hbs
add|ember-template-lint|no-down-event-binding|4|112|4|112|b3e87879e52edc06bb07f1ad0becc6ec762bbb39|1658102400000|1668474000000|1673658000000|app/components/modal-stripe-connect.hbs
add|ember-template-lint|no-down-event-binding|22|45|22|45|b3e87879e52edc06bb07f1ad0becc6ec762bbb39|1658102400000|1668474000000|1673658000000|app/components/modal-stripe-connect.hbs
add|ember-template-lint|no-action|4|53|4|53|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-suspend-user.hbs
add|ember-template-lint|no-action|11|41|11|41|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-suspend-user.hbs
add|ember-template-lint|no-action|21|39|21|39|c1c116347fcaf9f69d60fa7ae58d746e9994cdbd|1658102400000|1668474000000|1673658000000|app/components/modal-tier.hbs
add|ember-template-lint|no-action|36|39|36|39|7307d2c5d78fa732b959ced6823b3d82dcc07446|1658102400000|1668474000000|1673658000000|app/components/modal-tier.hbs
add|ember-template-lint|no-action|45|39|45|39|7307d2c5d78fa732b959ced6823b3d82dcc07446|1658102400000|1668474000000|1673658000000|app/components/modal-tier.hbs
add|ember-template-lint|no-action|73|52|73|52|fa36149ee581ce634903cef877ccfdc133aa67ec|1658102400000|1668474000000|1673658000000|app/components/modal-tier.hbs
add|ember-template-lint|no-action|85|47|85|47|dcc09bb23a476d5b83b273b693cd8cb2aba68365|1658102400000|1668474000000|1673658000000|app/components/modal-tier.hbs
add|ember-template-lint|no-action|95|47|95|47|a80dd18e18dda6fb6f1f97d87bef2b8c2ce3d847|1658102400000|1668474000000|1673658000000|app/components/modal-tier.hbs
add|ember-template-lint|no-action|133|47|133|47|73ac7d3892fcbcf15c3d5c44fca14dd21016daea|1658102400000|1668474000000|1673658000000|app/components/modal-tier.hbs
add|ember-template-lint|no-action|139|53|139|53|c76b92d7bdb6ed498238b647928748aa4146dc24|1658102400000|1668474000000|1673658000000|app/components/modal-tier.hbs
add|ember-template-lint|no-action|140|55|140|55|02efc45b808f4cfdbe5f7b72b784337aef4e98a7|1658102400000|1668474000000|1673658000000|app/components/modal-tier.hbs
add|ember-template-lint|no-action|141|56|141|56|4227b643fe44a6c343b819bbee7eecdfb7916ccc|1658102400000|1668474000000|1673658000000|app/components/modal-tier.hbs
add|ember-template-lint|no-action|142|57|142|57|115cc8adc4c0f98e2d93a59ff8efbee711541336|1658102400000|1668474000000|1673658000000|app/components/modal-tier.hbs
add|ember-template-lint|no-action|150|41|150|41|c76b92d7bdb6ed498238b647928748aa4146dc24|1658102400000|1668474000000|1673658000000|app/components/modal-tier.hbs
add|ember-template-lint|no-action|151|44|151|44|4227b643fe44a6c343b819bbee7eecdfb7916ccc|1658102400000|1668474000000|1673658000000|app/components/modal-tier.hbs
add|ember-template-lint|no-action|152|45|152|45|115cc8adc4c0f98e2d93a59ff8efbee711541336|1658102400000|1668474000000|1673658000000|app/components/modal-tier.hbs
add|ember-template-lint|no-action|267|71|267|71|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-tier.hbs
add|ember-template-lint|no-action|269|8|269|8|d465b362b15b90cf42a093e72895155f49cdf6f2|1658102400000|1668474000000|1673658000000|app/components/modal-tier.hbs
add|ember-template-lint|no-down-event-binding|269|41|269|41|b3e87879e52edc06bb07f1ad0becc6ec762bbb39|1658102400000|1668474000000|1673658000000|app/components/modal-tier.hbs
add|ember-template-lint|no-passed-in-event-handlers|21|32|21|32|f69395e36c890a23e3f603ad3fd2cb384932af93|1658102400000|1668474000000|1673658000000|app/components/modal-tier.hbs
add|ember-template-lint|no-passed-in-event-handlers|36|32|36|32|86b5983929a27ca8d458ff051c95a50a406fbe57|1658102400000|1668474000000|1673658000000|app/components/modal-tier.hbs
add|ember-template-lint|no-passed-in-event-handlers|45|32|45|32|86b5983929a27ca8d458ff051c95a50a406fbe57|1658102400000|1668474000000|1673658000000|app/components/modal-tier.hbs
add|ember-template-lint|no-passed-in-event-handlers|85|40|85|40|dcb4785647a50814bcfce82f8d68ac8dd8f54ec2|1658102400000|1668474000000|1673658000000|app/components/modal-tier.hbs
add|ember-template-lint|no-passed-in-event-handlers|95|40|95|40|70487c008d7dda453fef82f0140699ee93c0055c|1658102400000|1668474000000|1673658000000|app/components/modal-tier.hbs
add|ember-template-lint|style-concatenation|203|54|203|54|23293f0c3838b23432d2b2daaf04b34504896d91|1658102400000|1668474000000|1673658000000|app/components/modal-tier.hbs
add|ember-template-lint|no-action|4|53|4|53|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-transfer-owner.hbs
add|ember-template-lint|no-action|14|41|14|41|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-transfer-owner.hbs
add|ember-template-lint|no-action|4|53|4|53|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-unsubscribe-members.hbs
add|ember-template-lint|no-action|50|89|50|89|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-unsubscribe-members.hbs
add|ember-template-lint|no-action|54|71|54|71|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-unsubscribe-members.hbs
add|ember-template-lint|no-action|4|53|4|53|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-unsuspend-user.hbs
add|ember-template-lint|no-action|11|41|11|41|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-unsuspend-user.hbs
add|ember-template-lint|no-action|4|53|4|53|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-update-snippet.hbs
add|ember-template-lint|no-action|15|41|15|41|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-update-snippet.hbs
add|ember-template-lint|no-action|18|99|18|99|ed187c5c166b534cdd6e8c36a61ab1be5c2e259b|1658102400000|1668474000000|1673658000000|app/components/modal-upgrade-host-limit.hbs
add|ember-template-lint|no-action|17|99|17|99|ed187c5c166b534cdd6e8c36a61ab1be5c2e259b|1658102400000|1668474000000|1673658000000|app/components/modal-upgrade-unsuspend-user-host-limit.hbs
add|ember-template-lint|link-href-attributes|5|12|5|12|9a8d7ab37ceac5d53197768699f596ac2c5642de|1658102400000|1668474000000|1673658000000|app/components/modal-upload-image.hbs
add|ember-template-lint|no-action|5|51|5|51|772866626d8e0cff8ddfe113040a2bbd27a2e282|1658102400000|1668474000000|1673658000000|app/components/modal-upload-image.hbs
add|ember-template-lint|no-action|14|20|14|20|6df7472410762190612f82c70a94a954046e2007|1658102400000|1668474000000|1673658000000|app/components/modal-upload-image.hbs
add|ember-template-lint|no-action|15|27|15|27|8618e8ea6dd0bea8b895ac0c2af021760d87b37e|1658102400000|1668474000000|1673658000000|app/components/modal-upload-image.hbs
add|ember-template-lint|no-action|16|28|16|28|8618e8ea6dd0bea8b895ac0c2af021760d87b37e|1658102400000|1668474000000|1673658000000|app/components/modal-upload-image.hbs
add|ember-template-lint|no-action|26|41|26|41|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-upload-image.hbs
add|ember-template-lint|no-invalid-interactive|5|51|5|51|9a8d7ab37ceac5d53197768699f596ac2c5642de|1658102400000|1668474000000|1673658000000|app/components/modal-upload-image.hbs
add|ember-template-lint|require-valid-alt-text|4|17|4|17|7fc4a97a1c85df0febb29bec1ba348b8808533d3|1658102400000|1668474000000|1673658000000|app/components/modal-upload-image.hbs
add|ember-template-lint|no-action|4|55|4|55|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-webhook-form.hbs
add|ember-template-lint|no-action|4|79|4|79|d465b362b15b90cf42a093e72895155f49cdf6f2|1658102400000|1668474000000|1673658000000|app/components/modal-webhook-form.hbs
add|ember-template-lint|no-action|14|23|14|23|616c6bc12166ed980b9deb2ffddb7ba609b2937b|1658102400000|1668474000000|1673658000000|app/components/modal-webhook-form.hbs
add|ember-template-lint|no-action|15|27|15|27|676e64c434e820dde7c21b5edda3c5aab42e5888|1658102400000|1668474000000|1673658000000|app/components/modal-webhook-form.hbs
add|ember-template-lint|no-action|39|28|39|28|3aa7d43ab2337652cbf63e9757c7faa11ef25145|1658102400000|1668474000000|1673658000000|app/components/modal-webhook-form.hbs
add|ember-template-lint|no-action|53|23|53|23|7f08ac4e0471f57368848fc50f851dcce4a03657|1658102400000|1668474000000|1673658000000|app/components/modal-webhook-form.hbs
add|ember-template-lint|no-action|54|27|54|27|a85c312488046bec6b27be0107bfb558a7666976|1658102400000|1668474000000|1673658000000|app/components/modal-webhook-form.hbs
add|ember-template-lint|no-action|72|29|72|29|9ad7747dc8552fb38b69f68671439515bacfb215|1658102400000|1668474000000|1673658000000|app/components/modal-webhook-form.hbs
add|ember-template-lint|no-action|73|31|73|31|783636572e0ed93e11919c197d6eecf1c8c7ee01|1658102400000|1668474000000|1673658000000|app/components/modal-webhook-form.hbs
add|ember-template-lint|no-action|92|71|92|71|141d456b03124abca146e58e4ae15825fdd040bb|1658102400000|1668474000000|1673658000000|app/components/modal-webhook-form.hbs
add|ember-template-lint|no-action|94|8|94|8|d465b362b15b90cf42a093e72895155f49cdf6f2|1658102400000|1668474000000|1673658000000|app/components/modal-webhook-form.hbs
add|ember-template-lint|no-down-event-binding|4|112|4|112|b3e87879e52edc06bb07f1ad0becc6ec762bbb39|1658102400000|1668474000000|1673658000000|app/components/modal-webhook-form.hbs
add|ember-template-lint|no-down-event-binding|94|41|94|41|b3e87879e52edc06bb07f1ad0becc6ec762bbb39|1658102400000|1668474000000|1673658000000|app/components/modal-webhook-form.hbs
add|ember-template-lint|no-passed-in-event-handlers|14|16|14|16|6bcbd72f8170ee033d443381f42149f02fe4e3a3|1658102400000|1668474000000|1673658000000|app/components/modal-webhook-form.hbs
add|ember-template-lint|no-passed-in-event-handlers|53|16|53|16|7b5b565dd7c0feea2efc926c9acd32060678c547|1658102400000|1668474000000|1673658000000|app/components/modal-webhook-form.hbs
add|ember-template-lint|require-valid-alt-text|4|12|4|12|8369d1b06deac93e8c8e05444670c15182aea434|1658102400000|1668474000000|1673658000000|app/templates/application-error.hbs
add|ember-template-lint|no-passed-in-event-handlers|34|36|34|36|55f7b92a44dc897758496ba1560e34a1d53fb946|1658102400000|1668474000000|1673658000000|app/templates/offer.hbs
add|ember-template-lint|no-passed-in-event-handlers|76|52|76|52|37bf29e93ffc35c71cdddd0ab98edeb60097e826|1658102400000|1668474000000|1673658000000|app/templates/offer.hbs
add|ember-template-lint|no-passed-in-event-handlers|87|52|87|52|37bf29e93ffc35c71cdddd0ab98edeb60097e826|1658102400000|1668474000000|1673658000000|app/templates/offer.hbs
add|ember-template-lint|no-passed-in-event-handlers|142|44|142|44|eb97a902a644abbe2df5bc2e57f7affa0a96fdac|1658102400000|1668474000000|1673658000000|app/templates/offer.hbs
add|ember-template-lint|no-passed-in-event-handlers|163|40|163|40|5c7497369e17fd561d43dd7edad9c0887c21e9ff|1658102400000|1668474000000|1673658000000|app/templates/offer.hbs
add|ember-template-lint|no-passed-in-event-handlers|177|40|177|40|f6273c4d79a992751e950f5793ddf17bcf03e649|1658102400000|1668474000000|1673658000000|app/templates/offer.hbs
add|ember-template-lint|no-passed-in-event-handlers|192|36|192|36|5257e566aba16d7d9a28cf1df37a625c1eebfb25|1658102400000|1668474000000|1673658000000|app/templates/offer.hbs
add|ember-template-lint|no-invalid-interactive|81|36|81|36|fc4eb64cc0ad0cc9c500c7ef026e44649bc4778b|1658102400000|1668474000000|1673658000000|app/templates/offers.hbs
add|ember-template-lint|table-groups|29|12|29|12|49e0fad85b643b6129963e49b6b5a37ccbd9796c|1658102400000|1668474000000|1673658000000|app/templates/offers.hbs
add|ember-template-lint|no-action|1|40|1|40|180177e5e700535031498faa0ea4f1715555bfad|1658102400000|1668474000000|1673658000000|app/templates/pages-loading.hbs
add|ember-template-lint|no-action|10|30|10|30|796968d082daba388ca19e97d45c7278d1076f04|1658102400000|1668474000000|1673658000000|app/templates/pages-loading.hbs
add|ember-template-lint|no-action|13|36|13|36|796968d082daba388ca19e97d45c7278d1076f04|1658102400000|1668474000000|1673658000000|app/templates/pages-loading.hbs
add|ember-template-lint|no-action|16|32|16|32|796968d082daba388ca19e97d45c7278d1076f04|1658102400000|1668474000000|1673658000000|app/templates/pages-loading.hbs
add|ember-template-lint|no-action|19|29|19|29|796968d082daba388ca19e97d45c7278d1076f04|1658102400000|1668474000000|1673658000000|app/templates/pages-loading.hbs
add|ember-template-lint|no-action|22|31|22|31|796968d082daba388ca19e97d45c7278d1076f04|1658102400000|1668474000000|1673658000000|app/templates/pages-loading.hbs
add|ember-template-lint|no-action|10|30|10|30|7f6ae6475e94edfe22b59577ca52d10ec66835af|1658102400000|1668474000000|1673658000000|app/templates/pages.hbs
add|ember-template-lint|no-action|13|36|13|36|addf24b8fbd6518060e412ec8b95309a05254988|1658102400000|1668474000000|1673658000000|app/templates/pages.hbs
add|ember-template-lint|no-action|16|32|16|32|5bf020bb7cafdcfbcf809310232be927a9f1b2e4|1658102400000|1668474000000|1673658000000|app/templates/pages.hbs
add|ember-template-lint|no-action|19|29|19|29|e3023c525990c1f8d1a30aa6ff20eb3bad8ec53b|1658102400000|1668474000000|1673658000000|app/templates/pages.hbs
add|ember-template-lint|no-action|22|31|22|31|cbf4e21da2010b9af11294f458ef2ad40374d4f0|1658102400000|1668474000000|1673658000000|app/templates/pages.hbs
add|ember-template-lint|no-action|1|40|1|40|180177e5e700535031498faa0ea4f1715555bfad|1658102400000|1668474000000|1673658000000|app/templates/posts-loading.hbs
add|ember-template-lint|no-action|9|30|9|30|796968d082daba388ca19e97d45c7278d1076f04|1658102400000|1668474000000|1673658000000|app/templates/posts-loading.hbs
add|ember-template-lint|no-action|12|36|12|36|796968d082daba388ca19e97d45c7278d1076f04|1658102400000|1668474000000|1673658000000|app/templates/posts-loading.hbs
add|ember-template-lint|no-action|15|32|15|32|796968d082daba388ca19e97d45c7278d1076f04|1658102400000|1668474000000|1673658000000|app/templates/posts-loading.hbs
add|ember-template-lint|no-action|18|29|18|29|796968d082daba388ca19e97d45c7278d1076f04|1658102400000|1668474000000|1673658000000|app/templates/posts-loading.hbs
add|ember-template-lint|no-action|21|31|21|31|796968d082daba388ca19e97d45c7278d1076f04|1658102400000|1668474000000|1673658000000|app/templates/posts-loading.hbs
add|ember-template-lint|no-action|10|30|10|30|7f6ae6475e94edfe22b59577ca52d10ec66835af|1658102400000|1668474000000|1673658000000|app/templates/posts.hbs
add|ember-template-lint|no-action|13|36|13|36|addf24b8fbd6518060e412ec8b95309a05254988|1658102400000|1668474000000|1673658000000|app/templates/posts.hbs
add|ember-template-lint|no-action|16|32|16|32|5bf020bb7cafdcfbcf809310232be927a9f1b2e4|1658102400000|1668474000000|1673658000000|app/templates/posts.hbs
add|ember-template-lint|no-action|19|29|19|29|e3023c525990c1f8d1a30aa6ff20eb3bad8ec53b|1658102400000|1668474000000|1673658000000|app/templates/posts.hbs
add|ember-template-lint|no-action|22|31|22|31|cbf4e21da2010b9af11294f458ef2ad40374d4f0|1658102400000|1668474000000|1673658000000|app/templates/posts.hbs
add|ember-template-lint|no-action|4|85|4|85|ef910eebe8656965ebc73588c6f34a6260006b96|1658102400000|1668474000000|1673658000000|app/templates/reset.hbs
add|ember-template-lint|no-action|19|35|19|35|8b04fb9251c6a34b6bfc2995b527539bb4106c37|1658102400000|1668474000000|1673658000000|app/templates/reset.hbs
add|ember-template-lint|no-action|31|35|31|35|ddfad6e48c0df2368eed5dd9faf83f2a96dc182e|1658102400000|1668474000000|1673658000000|app/templates/reset.hbs
add|ember-template-lint|no-passed-in-event-handlers|19|28|19|28|ea0378c5df53f2be82f0fd59e0c538efd7176851|1658102400000|1668474000000|1673658000000|app/templates/reset.hbs
add|ember-template-lint|no-passed-in-event-handlers|31|28|31|28|06311a82a9589d4c126863466d56c7c4a76409d0|1658102400000|1668474000000|1673658000000|app/templates/reset.hbs
add|ember-template-lint|no-action|91|17|91|17|c09dcb3dfe6ae3f5802c4ede66c922ed7b6a94e5|1658102400000|1668474000000|1673658000000|app/templates/settings.hbs
add|ember-template-lint|no-action|92|15|92|15|4327a1a496a49bd7460b2972a934a3bfbfb40ce0|1658102400000|1668474000000|1673658000000|app/templates/settings.hbs
add|ember-template-lint|no-action|20|31|20|31|3aa834e53af871a821ebb1b47f7a04f925c2b593|1658102400000|1668474000000|1673658000000|app/templates/setup.hbs
add|ember-template-lint|no-action|21|35|21|35|ae65e93ed2b79a307e0eba50b154fe84ee1ae210|1658102400000|1668474000000|1673658000000|app/templates/setup.hbs
add|ember-template-lint|no-action|37|31|37|31|eefdee43411bdd45c2786b9e826e6b550fd6212d|1658102400000|1668474000000|1673658000000|app/templates/setup.hbs
add|ember-template-lint|no-action|38|35|38|35|a8e4bd4c57a8f2df65749b0cfa4349da22b2a0aa|1658102400000|1668474000000|1673658000000|app/templates/setup.hbs
add|ember-template-lint|no-action|55|31|55|31|1709109776bf3fc47aaecc21e6d3ec8a0489ad6b|1658102400000|1668474000000|1673658000000|app/templates/setup.hbs
add|ember-template-lint|no-action|56|35|56|35|8000241a81189d3876d264331ec0c9b6476df076|1658102400000|1668474000000|1673658000000|app/templates/setup.hbs
add|ember-template-lint|no-action|73|31|73|31|6756e119daad4aa143724e9b56a6aef744d65251|1658102400000|1668474000000|1673658000000|app/templates/setup.hbs
add|ember-template-lint|no-action|74|35|74|35|26b1d89c0e5bbcd629a215903c0819d35f798aa6|1658102400000|1668474000000|1673658000000|app/templates/setup.hbs
add|ember-template-lint|no-passed-in-event-handlers|20|24|20|24|f0b7babba7593639d68dadae2f19e7144dd8c63e|1658102400000|1668474000000|1673658000000|app/templates/setup.hbs
add|ember-template-lint|no-passed-in-event-handlers|37|24|37|24|2692530760cb1f7156dcf9570aacf0f8baca2770|1658102400000|1668474000000|1673658000000|app/templates/setup.hbs
add|ember-template-lint|no-passed-in-event-handlers|55|24|55|24|ce0d7e2e732b22ce3643fb9b1ac6ecef07c275ff|1658102400000|1668474000000|1673658000000|app/templates/setup.hbs
add|ember-template-lint|no-passed-in-event-handlers|73|24|73|24|80681fcec2258c3d81a231bbd635aa1f03ff1452|1658102400000|1668474000000|1673658000000|app/templates/setup.hbs
add|ember-template-lint|no-action|15|85|15|85|5f277699a01bbbcf2e74101096f9c9bff189af74|1658102400000|1668474000000|1673658000000|app/templates/signin.hbs
add|ember-template-lint|no-action|33|35|33|35|85790bc3673715479aa2678f29d8a61e47281bc7|1658102400000|1668474000000|1673658000000|app/templates/signin.hbs
add|ember-template-lint|no-action|34|39|34|39|e65f48edccba27e52c1f8358a9795dc8ff20d5ef|1658102400000|1668474000000|1673658000000|app/templates/signin.hbs
add|ember-template-lint|no-action|50|35|50|35|7432725bd18c48f69bf22dc9487d14d25dc6c1b7|1658102400000|1668474000000|1673658000000|app/templates/signin.hbs
add|ember-template-lint|no-duplicate-landmark-elements|16|16|16|16|1661d2edb187b634c8187e5ecb0db15a4c7262fc|1658102400000|1668474000000|1673658000000|app/templates/signin.hbs
add|ember-template-lint|no-passed-in-event-handlers|33|28|33|28|5b371baf419f247953b91b626611cb831c524af3|1658102400000|1668474000000|1673658000000|app/templates/signin.hbs
add|ember-template-lint|no-passed-in-event-handlers|50|28|50|28|40caf07c7cebf6f4321c5b7e7f2f426b5c30217b|1658102400000|1668474000000|1673658000000|app/templates/signin.hbs
add|ember-template-lint|no-action|17|25|17|25|fc6f0949f991c50fdeb7e2b42d25cd1169cf311c|1658102400000|1668474000000|1673658000000|app/templates/tag.hbs
add|ember-template-lint|no-action|22|85|22|85|8024e7e42cf37a5954ea4db401cbf123931da388|1658102400000|1668474000000|1673658000000|app/templates/tag.hbs
add|ember-template-lint|no-action|32|17|32|17|9fa650a0ca3325ef8182634ad5d34c5db548287a|1658102400000|1668474000000|1673658000000|app/templates/tag.hbs
add|ember-template-lint|no-action|33|15|33|15|2fe6989da74d54cbdbaef0ae0a7cc68f22825a1d|1658102400000|1668474000000|1673658000000|app/templates/tag.hbs
add|ember-template-lint|no-action|41|17|41|17|712a767fa44489687284ef2aa32c1acc5221afce|1658102400000|1668474000000|1673658000000|app/templates/tag.hbs
add|ember-template-lint|no-action|42|15|42|15|d3fea3e454ca345739d46caf51afd85a3eea1041|1658102400000|1668474000000|1673658000000|app/templates/tag.hbs
add|ember-template-lint|no-action|1|40|1|40|573086f09d13f9bd362afc216e57873538ba48bd|1658102400000|1668474000000|1673658000000|app/templates/tags.hbs
add|ember-template-lint|no-action|6|108|6|108|ccc38f66549f9baedaa3b9943ae6634ea8f99e69|1658102400000|1668474000000|1673658000000|app/templates/tags.hbs
add|ember-template-lint|no-action|7|110|7|110|c3819ce2b6989e8596be570ed0c9fb82b5012521|1658102400000|1668474000000|1673658000000|app/templates/tags.hbs
add|ember-template-lint|no-nested-interactive|23|28|23|28|5cf783a5684dda036706ff7438472e99c60a88e7|1658102400000|1668474000000|1673658000000|app/templates/whatsnew.hbs
add|ember-template-lint|no-triple-curlies|25|28|25|28|9f944c7207ff6f368ea28436c94ff67977fb6823|1658102400000|1668474000000|1673658000000|app/templates/whatsnew.hbs
add|ember-template-lint|require-valid-alt-text|18|28|18|28|80c1ce6724481312363dc4e1db42bf28b41909f2|1658102400000|1668474000000|1673658000000|app/templates/whatsnew.hbs
add|ember-template-lint|require-input-label|28|20|28|20|dc08ecbcb2f3358fe94e7e2b8f24f7ca17bf77b5|1658102400000|1668474000000|1673658000000|app/components/custom-theme-settings/color.hbs
add|ember-template-lint|no-invalid-interactive|19|71|19|71|c1a223c4a0c265ed64685dbfbdd39581ba237083|1658102400000|1668474000000|1673658000000|app/components/custom-theme-settings/image.hbs
add|ember-template-lint|require-valid-alt-text|19|24|19|24|c1a223c4a0c265ed64685dbfbdd39581ba237083|1658102400000|1668474000000|1673658000000|app/components/custom-theme-settings/image.hbs
add|ember-template-lint|no-autofocus-attribute|14|4|14|4|bc0a12a01d16038dd3224726d5c4fe81b2d458b6|1658102400000|1668474000000|1673658000000|app/components/gh-input-with-select/trigger.hbs
add|ember-template-lint|no-down-event-binding|8|9|8|9|e82f6aa36fd44bb3dccff09770613eee19380f9b|1658102400000|1668474000000|1673658000000|app/components/gh-input-with-select/trigger.hbs
add|ember-template-lint|require-input-label|4|0|4|0|ce9c488b3c6a110afe45e2242fb068fd4ed61f22|1658102400000|1668474000000|1673658000000|app/components/gh-input-with-select/trigger.hbs
add|ember-template-lint|no-action|63|39|63|39|dcc09bb23a476d5b83b273b693cd8cb2aba68365|1658102400000|1668474000000|1673658000000|app/components/gh-launch-wizard/set-pricing.hbs
add|ember-template-lint|no-action|78|39|78|39|a80dd18e18dda6fb6f1f97d87bef2b8c2ce3d847|1658102400000|1668474000000|1673658000000|app/components/gh-launch-wizard/set-pricing.hbs
add|ember-template-lint|no-passed-in-event-handlers|63|32|63|32|dcb4785647a50814bcfce82f8d68ac8dd8f54ec2|1658102400000|1668474000000|1673658000000|app/components/gh-launch-wizard/set-pricing.hbs
add|ember-template-lint|no-passed-in-event-handlers|78|32|78|32|70487c008d7dda453fef82f0140699ee93c0055c|1658102400000|1668474000000|1673658000000|app/components/gh-launch-wizard/set-pricing.hbs
add|ember-template-lint|no-invalid-interactive|6|32|6|32|508e64575a985432d0588f3291a126c4b62e68d8|1658102400000|1668474000000|1673658000000|app/components/gh-nav-menu/design.hbs
add|ember-template-lint|no-action|97|83|97|83|40a33b8afd29e93eebeaccdb49d729c06f755e1f|1658102400000|1668474000000|1673658000000|app/components/gh-nav-menu/footer.hbs
add|ember-template-lint|no-invalid-interactive|97|83|97|83|91111b837d1217ec9988076f5263b0e32df72604|1658102400000|1668474000000|1673658000000|app/components/gh-nav-menu/footer.hbs
add|ember-template-lint|no-unknown-arguments-for-builtin-components|29|104|29|104|156670ca427c49c51f0a94f862b286ccc9466d92|1658102400000|1668474000000|1673658000000|app/components/gh-nav-menu/footer.hbs
add|ember-template-lint|no-unknown-arguments-for-builtin-components|39|107|39|107|156670ca427c49c51f0a94f862b286ccc9466d92|1658102400000|1668474000000|1673658000000|app/components/gh-nav-menu/footer.hbs
add|ember-template-lint|no-unknown-arguments-for-builtin-components|49|121|49|121|156670ca427c49c51f0a94f862b286ccc9466d92|1658102400000|1668474000000|1673658000000|app/components/gh-nav-menu/footer.hbs
add|ember-template-lint|no-unknown-arguments-for-builtin-components|84|93|84|93|156670ca427c49c51f0a94f862b286ccc9466d92|1658102400000|1668474000000|1673658000000|app/components/gh-nav-menu/footer.hbs
add|ember-template-lint|require-context-role|34|89|34|89|0be75355d0dd43dafc60091285ba906c43350d19|1658102400000|1668474000000|1673658000000|app/components/gh-nav-menu/footer.hbs
add|ember-template-lint|require-context-role|57|57|57|57|0be75355d0dd43dafc60091285ba906c43350d19|1658102400000|1668474000000|1673658000000|app/components/gh-nav-menu/footer.hbs
add|ember-template-lint|require-context-role|62|57|62|57|0be75355d0dd43dafc60091285ba906c43350d19|1658102400000|1668474000000|1673658000000|app/components/gh-nav-menu/footer.hbs
add|ember-template-lint|require-context-role|73|131|73|131|0be75355d0dd43dafc60091285ba906c43350d19|1658102400000|1668474000000|1673658000000|app/components/gh-nav-menu/footer.hbs
add|ember-template-lint|no-action|10|110|10|110|4bbb6ad1f623335866ac715f34f2ce9829b1d55b|1658102400000|1668474000000|1673658000000|app/components/gh-nav-menu/main.hbs
add|ember-template-lint|no-action|26|30|26|30|8eee88dbd40609f8ddc00330f4a47b1d30c483f0|1658102400000|1668474000000|1673658000000|app/components/gh-nav-menu/main.hbs
add|ember-template-lint|no-action|121|110|121|110|c8306856104d54d12e3db50acab26094d0793fb3|1658102400000|1668474000000|1673658000000|app/components/gh-nav-menu/main.hbs
add|ember-template-lint|no-forbidden-elements|147|20|147|20|966511a5bc154d46999a9594e3d39eb6ee5028a8|1658102400000|1668474000000|1673658000000|app/components/gh-nav-menu/main.hbs
add|ember-template-lint|no-invalid-interactive|26|30|26|30|929269f70336deb9f640bc0b5d54a0bd5336d27a|1658102400000|1668474000000|1673658000000|app/components/gh-nav-menu/main.hbs
add|ember-template-lint|no-invalid-link-title|22|24|22|24|17a357b69040eb9e19a79fe08468c698eab84939|1658102400000|1668474000000|1673658000000|app/components/gh-nav-menu/main.hbs
add|ember-template-lint|no-triple-curlies|146|20|146|20|9a305980cd2c3469773e4adafa4e02a949dbc919|1658102400000|1668474000000|1673658000000|app/components/gh-nav-menu/main.hbs
add|ember-template-lint|no-unknown-arguments-for-builtin-components|22|51|22|51|b8aae2daed1c14cf280800b3d282d11fb14851a4|1658102400000|1668474000000|1673658000000|app/components/gh-nav-menu/main.hbs
add|ember-template-lint|no-unknown-arguments-for-builtin-components|27|103|27|103|571c3d774ed33480f528b54b323b23bfd7148eee|1658102400000|1668474000000|1673658000000|app/components/gh-nav-menu/main.hbs
add|ember-template-lint|no-unknown-arguments-for-builtin-components|39|110|39|110|b8aae2daed1c14cf280800b3d282d11fb14851a4|1658102400000|1668474000000|1673658000000|app/components/gh-nav-menu/main.hbs
add|ember-template-lint|no-unknown-arguments-for-builtin-components|111|56|111|56|b8aae2daed1c14cf280800b3d282d11fb14851a4|1658102400000|1668474000000|1673658000000|app/components/gh-nav-menu/main.hbs
add|ember-template-lint|no-action|96|14|96|14|3d44a2ad21d60d18bed6f79265caa4887361aaa4|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu/email.hbs
add|ember-template-lint|no-action|105|27|105|27|5ac8ef12ae9cda0ec21b12a86319e34a870277de|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu/email.hbs
add|ember-template-lint|no-action|106|31|106|31|134228426e26bc169dc65108012ffef70c0bb46a|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu/email.hbs
add|ember-template-lint|no-passed-in-event-handlers|105|20|105|20|cd7c5bd8b1c45775a6dff5c502055d5522929b32|1658102400000|1668474000000|1673658000000|app/components/gh-post-settings-menu/email.hbs
add|ember-template-lint|no-down-event-binding|8|56|8|56|a158be60fde14211f6abfbac329c94fa5a3b4a46|1658102400000|1668474000000|1673658000000|app/components/gh-power-select/trigger.hbs
add|ember-template-lint|no-invalid-interactive|8|51|8|51|66a27dbed218d15e49e91c72a93215ad0d90f778|1658102400000|1668474000000|1673658000000|app/components/gh-power-select/trigger.hbs
add|ember-template-lint|no-yield-only|1|0|1|0|a5fa6e8c1e0f03fb31b5cd17770e4368d44932e4|1658102400000|1668474000000|1673658000000|app/components/gh-token-input/label-token.hbs
add|ember-template-lint|no-yield-only|1|0|1|0|a5fa6e8c1e0f03fb31b5cd17770e4368d44932e4|1658102400000|1668474000000|1673658000000|app/components/gh-token-input/tag-token.hbs
add|ember-template-lint|no-action|8|19|8|19|73ac7d3892fcbcf15c3d5c44fca14dd21016daea|1658102400000|1668474000000|1673658000000|app/components/gh-token-input/trigger.hbs
add|ember-template-lint|no-down-event-binding|31|25|31|25|a158be60fde14211f6abfbac329c94fa5a3b4a46|1658102400000|1668474000000|1673658000000|app/components/gh-token-input/trigger.hbs
add|ember-template-lint|no-down-event-binding|61|17|61|17|e82f6aa36fd44bb3dccff09770613eee19380f9b|1658102400000|1668474000000|1673658000000|app/components/gh-token-input/trigger.hbs
add|ember-template-lint|no-positive-tabindex|44|8|44|8|6118264a9a0599fab6ad4da7264c1bfffa88687e|1658102400000|1668474000000|1673658000000|app/components/gh-token-input/trigger.hbs
add|ember-template-lint|no-down-event-binding|6|86|6|86|a158be60fde14211f6abfbac329c94fa5a3b4a46|1658102400000|1668474000000|1673658000000|app/components/modals/custom-view-form.hbs
add|ember-template-lint|no-down-event-binding|55|21|55|21|a158be60fde14211f6abfbac329c94fa5a3b4a46|1658102400000|1668474000000|1673658000000|app/components/modals/custom-view-form.hbs
add|ember-template-lint|no-down-event-binding|62|21|62|21|a158be60fde14211f6abfbac329c94fa5a3b4a46|1658102400000|1668474000000|1673658000000|app/components/modals/custom-view-form.hbs
add|ember-template-lint|require-iframe-title|27|20|27|20|94e58d11848d5613900c2188ba63dac41f4c03bb|1658102400000|1668474000000|1673658000000|app/components/modals/email-preview.hbs
add|ember-template-lint|require-iframe-title|42|16|42|16|a3292b469dc37f2f4791e7f224b0b65c8ecf5d18|1658102400000|1668474000000|1673658000000|app/components/modals/email-preview.hbs
add|ember-template-lint|no-autofocus-attribute|21|20|21|20|942419d05c04ded6716f09faecd6b1ab55418121|1658102400000|1668474000000|1673658000000|app/components/modals/new-custom-integration.hbs
add|ember-template-lint|no-invalid-interactive|2|37|2|37|e21ba31f54b631a428c28a1c9f88d0dc66f2f5fc|1658102400000|1668474000000|1673658000000|app/components/modals/search.hbs
add|ember-template-lint|no-action|31|26|31|26|0e827c1770073f988535ceb9d6c49808a153d896|1658102400000|1668474000000|1673658000000|app/components/settings/members-default-post-access.hbs
add|ember-template-lint|no-action|15|21|15|21|df631bb317cb737790ce8fd9a9b107f5a196a10d|1658102400000|1668474000000|1673658000000|app/templates/settings/code-injection.hbs
add|ember-template-lint|no-action|16|19|16|19|35178fb4b4e564116d2012fda7d3977a471fc8e7|1658102400000|1668474000000|1673658000000|app/templates/settings/code-injection.hbs
add|ember-template-lint|no-action|32|190|32|190|ed86b04ad5f38f1a14076d74e0021268c98b3927|1658102400000|1668474000000|1673658000000|app/templates/settings/code-injection.hbs
add|ember-template-lint|no-action|40|190|40|190|2164918a2fce3dd29c513fd604fe36e3cef2b7c3|1658102400000|1668474000000|1673658000000|app/templates/settings/code-injection.hbs
add|ember-template-lint|no-action|15|21|15|21|df631bb317cb737790ce8fd9a9b107f5a196a10d|1658102400000|1668474000000|1673658000000|app/templates/settings/general.hbs
add|ember-template-lint|no-action|16|19|16|19|35178fb4b4e564116d2012fda7d3977a471fc8e7|1658102400000|1668474000000|1673658000000|app/templates/settings/general.hbs
add|ember-template-lint|no-action|30|57|30|57|55c24e7e54f416f3d86061f4f1c1ccaba8fbfc27|1658102400000|1668474000000|1673658000000|app/templates/settings/general.hbs
add|ember-template-lint|no-action|38|39|38|39|03e3188d8e2dfe0b2937d5b71883f016d07a4946|1658102400000|1668474000000|1673658000000|app/templates/settings/general.hbs
add|ember-template-lint|no-action|39|43|39|43|1c60e378fb19511c2cac50a7a42b6aee20605994|1658102400000|1668474000000|1673658000000|app/templates/settings/general.hbs
add|ember-template-lint|no-action|49|39|49|39|03463f0fb55cc878bd4c913e47d55f41e2947661|1658102400000|1668474000000|1673658000000|app/templates/settings/general.hbs
add|ember-template-lint|no-action|50|43|50|43|6cdbcde69f3d62415e293a78dbcf26b96fc36f45|1658102400000|1668474000000|1673658000000|app/templates/settings/general.hbs
add|ember-template-lint|no-action|67|57|67|57|f3fb07d2afa3090b0f97067437413284ed58f6c5|1658102400000|1668474000000|1673658000000|app/templates/settings/general.hbs
add|ember-template-lint|no-action|75|36|75|36|0232e593042bb48fcc89bf9ab0b91b242f264fba|1658102400000|1668474000000|1673658000000|app/templates/settings/general.hbs
add|ember-template-lint|no-action|87|57|87|57|6128361491f571a637abfda97ad687f73ab667c3|1658102400000|1668474000000|1673658000000|app/templates/settings/general.hbs
add|ember-template-lint|no-action|95|39|95|39|ba45af3e814e72ad4d95a8f317bb9cc9ad9215c8|1658102400000|1668474000000|1673658000000|app/templates/settings/general.hbs
add|ember-template-lint|no-action|96|43|96|43|2c1cc5f0c3aeddb4838344241fd5ad1706fa7f98|1658102400000|1668474000000|1673658000000|app/templates/settings/general.hbs
add|ember-template-lint|no-action|118|57|118|57|bf49560351459e5d6c9fde8cb2f052ce17b1b657|1658102400000|1668474000000|1673658000000|app/templates/settings/general.hbs
add|ember-template-lint|no-action|132|51|132|51|0a0ef4f73dbe59f49337893fdc92d2a55728ed07|1658102400000|1668474000000|1673658000000|app/templates/settings/general.hbs
add|ember-template-lint|no-action|145|51|145|51|c7ba1dbb7596398deeed5fb7a7fe3f7e57929ddb|1658102400000|1668474000000|1673658000000|app/templates/settings/general.hbs
add|ember-template-lint|no-action|180|57|180|57|d26ee22129821fa02604d8e2a93e9ae0a916639f|1658102400000|1668474000000|1673658000000|app/templates/settings/general.hbs
add|ember-template-lint|no-action|193|48|193|48|3732114c07810566f79f1afa68845ee20b92fa18|1658102400000|1668474000000|1673658000000|app/templates/settings/general.hbs
add|ember-template-lint|no-action|194|48|194|48|bc30430315dac2132453165473d30c5b2d80ba92|1658102400000|1668474000000|1673658000000|app/templates/settings/general.hbs
add|ember-template-lint|no-action|204|47|204|47|4345c59c19cd5b007ab34318b7fa11f34cca667a|1658102400000|1668474000000|1673658000000|app/templates/settings/general.hbs
add|ember-template-lint|no-action|215|47|215|47|44ee454f126f973a6a74fb5541a028ec8c35c670|1658102400000|1668474000000|1673658000000|app/templates/settings/general.hbs
add|ember-template-lint|no-action|268|57|268|57|84ed8480d5bf5a95046ccc582c64bc97f705018e|1658102400000|1668474000000|1673658000000|app/templates/settings/general.hbs
add|ember-template-lint|no-action|281|52|281|52|c6cdf101b8a83665bceda4c602acf68cdf3ef67b|1658102400000|1668474000000|1673658000000|app/templates/settings/general.hbs
add|ember-template-lint|no-action|282|52|282|52|1c05a27faf63c386a263092a5c7e44034a773719|1658102400000|1668474000000|1673658000000|app/templates/settings/general.hbs
add|ember-template-lint|no-action|292|51|292|51|3ca4850a38d0a79f31de327e4f0619cb4bd61305|1658102400000|1668474000000|1673658000000|app/templates/settings/general.hbs
add|ember-template-lint|no-action|303|51|303|51|97850bdbac4ce72904bbac5af258e64ccbeab822|1658102400000|1668474000000|1673658000000|app/templates/settings/general.hbs
add|ember-template-lint|no-action|357|57|357|57|e5c846bf3deb14ba03604a3d324a61d16248b453|1658102400000|1668474000000|1673658000000|app/templates/settings/general.hbs
add|ember-template-lint|no-action|368|39|368|39|1f32c13702a8b46410e9e10f9988fa2e0df41968|1658102400000|1668474000000|1673658000000|app/templates/settings/general.hbs
add|ember-template-lint|no-action|369|43|369|43|69b8d823159491abd67494eba728616ac0a928a5|1658102400000|1668474000000|1673658000000|app/templates/settings/general.hbs
add|ember-template-lint|no-action|381|39|381|39|226e659be4c2c10c0e5fbebb40f712f3016c9b90|1658102400000|1668474000000|1673658000000|app/templates/settings/general.hbs
add|ember-template-lint|no-action|382|43|382|43|d746c5532ce38d23d8537195a3afa6ce7235f577|1658102400000|1668474000000|1673658000000|app/templates/settings/general.hbs
add|ember-template-lint|no-action|413|40|413|40|3f4324c899d78620bab1b84fd1824e93c737c15c|1658102400000|1668474000000|1673658000000|app/templates/settings/general.hbs
add|ember-template-lint|no-action|431|47|431|47|866af142283d04b707df59f73fb539df98728301|1658102400000|1668474000000|1673658000000|app/templates/settings/general.hbs
add|ember-template-lint|no-action|432|43|432|43|674aac6769471bd8c724fab4dbd566cf220e1166|1658102400000|1668474000000|1673658000000|app/templates/settings/general.hbs
add|ember-template-lint|no-passed-in-event-handlers|38|32|38|32|cfeb8caa552eac5ebbb6d91854cb74f89cdf6e1a|1658102400000|1668474000000|1673658000000|app/templates/settings/general.hbs
add|ember-template-lint|no-passed-in-event-handlers|49|32|49|32|83c111cfc155ec3b6832e60e2d1a9414f6f75623|1658102400000|1668474000000|1673658000000|app/templates/settings/general.hbs
add|ember-template-lint|no-passed-in-event-handlers|95|32|95|32|c34c2e482b61c7ccd2b1103b5dee4b291eb2ec47|1658102400000|1668474000000|1673658000000|app/templates/settings/general.hbs
add|ember-template-lint|no-passed-in-event-handlers|132|44|132|44|615e13dcb9f6e380efec5f70a1bd4619c0ad1426|1658102400000|1668474000000|1673658000000|app/templates/settings/general.hbs
add|ember-template-lint|no-passed-in-event-handlers|145|44|145|44|f25458443d396a354c2971d0c90c2ddccf964fb1|1658102400000|1668474000000|1673658000000|app/templates/settings/general.hbs
add|ember-template-lint|no-passed-in-event-handlers|204|40|204|40|b48c77bffa2c66754ecbbb20b4d59b8ee317ae0f|1658102400000|1668474000000|1673658000000|app/templates/settings/general.hbs
add|ember-template-lint|no-passed-in-event-handlers|215|40|215|40|46e9644476d397c95936c4f2173ca0c4a214dd9e|1658102400000|1668474000000|1673658000000|app/templates/settings/general.hbs
add|ember-template-lint|no-passed-in-event-handlers|292|44|292|44|c6a6197b290f6b1e41cdc0b88a2571db7c025ddd|1658102400000|1668474000000|1673658000000|app/templates/settings/general.hbs
add|ember-template-lint|no-passed-in-event-handlers|303|44|303|44|3c119ddbb78d06c18da5b1e812440b28881a9448|1658102400000|1668474000000|1673658000000|app/templates/settings/general.hbs
add|ember-template-lint|no-passed-in-event-handlers|368|32|368|32|c14e20b2b090706f9af9fa416c4300c06e206894|1658102400000|1668474000000|1673658000000|app/templates/settings/general.hbs
add|ember-template-lint|no-passed-in-event-handlers|381|32|381|32|e286f50c92a17a31581207e4c65054f146cbd533|1658102400000|1668474000000|1673658000000|app/templates/settings/general.hbs
add|ember-template-lint|no-passed-in-event-handlers|432|36|432|36|60888a79c6c68107f859a65663f794408bc434f4|1658102400000|1668474000000|1673658000000|app/templates/settings/general.hbs
add|ember-template-lint|no-quoteless-attributes|372|123|372|123|7c9b71d827d85d0ffd6dbe85c9f2793c321f744a|1658102400000|1668474000000|1673658000000|app/templates/settings/general.hbs
add|ember-template-lint|no-quoteless-attributes|385|121|385|121|939e14038426a56465b8768f3b25f179400c3eee|1658102400000|1668474000000|1673658000000|app/templates/settings/general.hbs
add|ember-template-lint|no-quoteless-attributes|435|127|435|127|cf74da438ca6d134c28c9b80fecebd105f18f0ed|1658102400000|1668474000000|1673658000000|app/templates/settings/general.hbs
add|ember-template-lint|no-action|27|42|27|42|b60a8d95bc6d0e8c9f57d46647ab18947415d9dc|1658102400000|1668474000000|1673658000000|app/templates/settings/labs.hbs
add|ember-template-lint|no-action|66|61|66|61|aea6df58f8bc5e92ec7e059aff378cc785db5733|1658102400000|1668474000000|1673658000000|app/templates/settings/labs.hbs
add|ember-template-lint|no-action|76|82|76|82|81c7e8a314b8c2d62ceb49a48b20664192cdef22|1658102400000|1668474000000|1673658000000|app/templates/settings/labs.hbs
add|ember-template-lint|no-action|106|44|106|44|69e5f19c951bf70e9e7eaa72ea2b784ab97c3dff|1658102400000|1668474000000|1673658000000|app/templates/settings/labs.hbs
add|ember-template-lint|no-action|119|49|119|49|03987b6ed09575e15d2bac6a91e1d93d56125c51|1658102400000|1668474000000|1673658000000|app/templates/settings/labs.hbs
add|ember-template-lint|no-action|155|44|155|44|69e5f19c951bf70e9e7eaa72ea2b784ab97c3dff|1658102400000|1668474000000|1673658000000|app/templates/settings/labs.hbs
add|ember-template-lint|no-action|168|49|168|49|cc3eebe4d4988160eb63e445ccf7b62bcf122531|1658102400000|1668474000000|1673658000000|app/templates/settings/labs.hbs
add|ember-template-lint|no-action|237|15|237|15|81c7e8a314b8c2d62ceb49a48b20664192cdef22|1658102400000|1668474000000|1673658000000|app/templates/settings/labs.hbs
add|ember-template-lint|no-action|15|21|15|21|df631bb317cb737790ce8fd9a9b107f5a196a10d|1658102400000|1668474000000|1673658000000|app/templates/settings/navigation.hbs
add|ember-template-lint|no-action|16|19|16|19|35178fb4b4e564116d2012fda7d3977a471fc8e7|1658102400000|1668474000000|1673658000000|app/templates/settings/navigation.hbs
add|ember-template-lint|no-action|31|45|31|45|812038f28c7626209e8ae622703ea8c4e75b7f60|1658102400000|1668474000000|1673658000000|app/templates/settings/navigation.hbs
add|ember-template-lint|no-action|32|48|32|48|930d7fb74c5abe804a6b02736914e835c1b25777|1658102400000|1668474000000|1673658000000|app/templates/settings/navigation.hbs
add|ember-template-lint|no-action|33|47|33|47|3c723798059c71aedfa95cd70507bc0308728544|1658102400000|1668474000000|1673658000000|app/templates/settings/navigation.hbs
add|ember-template-lint|no-action|34|49|34|49|115cc8adc4c0f98e2d93a59ff8efbee711541336|1658102400000|1668474000000|1673658000000|app/templates/settings/navigation.hbs
add|ember-template-lint|no-action|42|33|42|33|812038f28c7626209e8ae622703ea8c4e75b7f60|1658102400000|1668474000000|1673658000000|app/templates/settings/navigation.hbs
add|ember-template-lint|no-action|43|35|43|35|3c723798059c71aedfa95cd70507bc0308728544|1658102400000|1668474000000|1673658000000|app/templates/settings/navigation.hbs
add|ember-template-lint|no-action|44|37|44|37|115cc8adc4c0f98e2d93a59ff8efbee711541336|1658102400000|1668474000000|1673658000000|app/templates/settings/navigation.hbs
add|ember-template-lint|no-action|60|45|60|45|812038f28c7626209e8ae622703ea8c4e75b7f60|1658102400000|1668474000000|1673658000000|app/templates/settings/navigation.hbs
add|ember-template-lint|no-action|61|48|61|48|930d7fb74c5abe804a6b02736914e835c1b25777|1658102400000|1668474000000|1673658000000|app/templates/settings/navigation.hbs
add|ember-template-lint|no-action|62|47|62|47|3c723798059c71aedfa95cd70507bc0308728544|1658102400000|1668474000000|1673658000000|app/templates/settings/navigation.hbs
add|ember-template-lint|no-action|63|49|63|49|115cc8adc4c0f98e2d93a59ff8efbee711541336|1658102400000|1668474000000|1673658000000|app/templates/settings/navigation.hbs
add|ember-template-lint|no-action|71|33|71|33|812038f28c7626209e8ae622703ea8c4e75b7f60|1658102400000|1668474000000|1673658000000|app/templates/settings/navigation.hbs
add|ember-template-lint|no-action|72|35|72|35|3c723798059c71aedfa95cd70507bc0308728544|1658102400000|1668474000000|1673658000000|app/templates/settings/navigation.hbs
add|ember-template-lint|no-action|73|37|73|37|115cc8adc4c0f98e2d93a59ff8efbee711541336|1658102400000|1668474000000|1673658000000|app/templates/settings/navigation.hbs
add|ember-template-lint|no-duplicate-landmark-elements|53|16|53|16|f2740bc03b393e8708035d4952a2ab630472bd22|1658102400000|1668474000000|1673658000000|app/templates/settings/navigation.hbs
add|ember-template-lint|simple-unless|7|16|7|16|02b279bc993a1ee6632c7e6edf341a2aabc519cb|1658102400000|1668474000000|1673658000000|app/components/modals/design/theme-errors.hbs
add|ember-template-lint|no-action|17|21|17|21|df631bb317cb737790ce8fd9a9b107f5a196a10d|1658102400000|1668474000000|1673658000000|app/templates/settings/integrations/amp.hbs
add|ember-template-lint|no-action|18|19|18|19|35178fb4b4e564116d2012fda7d3977a471fc8e7|1658102400000|1668474000000|1673658000000|app/templates/settings/integrations/amp.hbs
add|ember-template-lint|no-action|53|52|53|52|2b83cff3852a6f3cb1f2d2613e1bb611e1af4996|1658102400000|1668474000000|1673658000000|app/templates/settings/integrations/amp.hbs
add|ember-template-lint|no-action|73|54|73|54|2fc1814780dbf5fab6c8d588de3644dce925acbc|1658102400000|1668474000000|1673658000000|app/templates/settings/integrations/amp.hbs
add|ember-template-lint|require-valid-alt-text|26|20|26|20|91e015c567087d47025b0e457150eb9d6a3a8f1c|1658102400000|1668474000000|1673658000000|app/templates/settings/integrations/amp.hbs
add|ember-template-lint|no-action|17|21|17|21|df631bb317cb737790ce8fd9a9b107f5a196a10d|1658102400000|1668474000000|1673658000000|app/templates/settings/integrations/firstpromoter.hbs
add|ember-template-lint|no-action|18|19|18|19|35178fb4b4e564116d2012fda7d3977a471fc8e7|1658102400000|1668474000000|1673658000000|app/templates/settings/integrations/firstpromoter.hbs
add|ember-template-lint|no-action|53|48|53|48|2b83cff3852a6f3cb1f2d2613e1bb611e1af4996|1658102400000|1668474000000|1673658000000|app/templates/settings/integrations/firstpromoter.hbs
add|ember-template-lint|no-action|73|50|73|50|2fc1814780dbf5fab6c8d588de3644dce925acbc|1658102400000|1668474000000|1673658000000|app/templates/settings/integrations/firstpromoter.hbs
add|ember-template-lint|require-valid-alt-text|26|20|26|20|6cfcbac2f373de035261f93c852c6d6d372b6ce4|1658102400000|1668474000000|1673658000000|app/templates/settings/integrations/firstpromoter.hbs
add|ember-template-lint|no-action|17|21|17|21|df631bb317cb737790ce8fd9a9b107f5a196a10d|1658102400000|1668474000000|1673658000000|app/templates/settings/integrations/slack.hbs
add|ember-template-lint|no-action|18|19|18|19|35178fb4b4e564116d2012fda7d3977a471fc8e7|1658102400000|1668474000000|1673658000000|app/templates/settings/integrations/slack.hbs
add|ember-template-lint|no-action|39|94|39|94|3230630f00a597aa98dfb1dd3533375d55516667|1658102400000|1668474000000|1673658000000|app/templates/settings/integrations/slack.hbs
add|ember-template-lint|no-action|52|55|52|55|2c1e9f8787439af0488bbeeed6adc276a525e585|1658102400000|1668474000000|1673658000000|app/templates/settings/integrations/slack.hbs
add|ember-template-lint|no-action|53|59|53|59|126db4f8e2a36d157b01c874381f548fd8d4a79e|1658102400000|1668474000000|1673658000000|app/templates/settings/integrations/slack.hbs
add|ember-template-lint|no-action|55|58|55|58|2fc1814780dbf5fab6c8d588de3644dce925acbc|1658102400000|1668474000000|1673658000000|app/templates/settings/integrations/slack.hbs
add|ember-template-lint|no-action|78|55|78|55|f790efaece9bf04396b25c2fedf4625a426f2467|1658102400000|1668474000000|1673658000000|app/templates/settings/integrations/slack.hbs
add|ember-template-lint|no-action|80|58|80|58|2fc1814780dbf5fab6c8d588de3644dce925acbc|1658102400000|1668474000000|1673658000000|app/templates/settings/integrations/slack.hbs
add|ember-template-lint|no-passed-in-event-handlers|52|48|52|48|138f75fcc7408e61c79185c37265529c752af8b3|1658102400000|1668474000000|1673658000000|app/templates/settings/integrations/slack.hbs
add|ember-template-lint|no-passed-in-event-handlers|78|48|78|48|8fb95409c6333d15490f0777239f6e04641215e6|1658102400000|1668474000000|1673658000000|app/templates/settings/integrations/slack.hbs
add|ember-template-lint|require-valid-alt-text|26|20|26|20|a500be6114ce681e610b6fec7d2a6cea2f4555f7|1658102400000|1668474000000|1673658000000|app/templates/settings/integrations/slack.hbs
add|ember-template-lint|simple-unless|61|44|61|44|02b279bc993a1ee6632c7e6edf341a2aabc519cb|1658102400000|1668474000000|1673658000000|app/templates/settings/integrations/slack.hbs
add|ember-template-lint|no-action|17|21|17|21|df631bb317cb737790ce8fd9a9b107f5a196a10d|1658102400000|1668474000000|1673658000000|app/templates/settings/integrations/unsplash.hbs
add|ember-template-lint|no-action|18|19|18|19|35178fb4b4e564116d2012fda7d3977a471fc8e7|1658102400000|1668474000000|1673658000000|app/templates/settings/integrations/unsplash.hbs
add|ember-template-lint|no-action|53|56|53|56|2b83cff3852a6f3cb1f2d2613e1bb611e1af4996|1658102400000|1668474000000|1673658000000|app/templates/settings/integrations/unsplash.hbs
add|ember-template-lint|require-valid-alt-text|26|20|26|20|c8f3616a9f2eef8909029c4ffdedc5886064d6ca|1658102400000|1668474000000|1673658000000|app/templates/settings/integrations/unsplash.hbs
add|ember-template-lint|no-action|33|66|33|66|2e441eb8956fdd6684b75ae1139f097237ebc1fa|1658102400000|1668474000000|1673658000000|app/templates/settings/integrations/zapier.hbs
add|ember-template-lint|no-action|36|66|36|66|2d51bed033fe50baea3002bf5acc234ac3738b58|1658102400000|1668474000000|1673658000000|app/templates/settings/integrations/zapier.hbs
add|ember-template-lint|no-action|58|66|58|66|bfcab210b651dbe74a1b9e5eb565756ab863f3bb|1658102400000|1668474000000|1673658000000|app/templates/settings/integrations/zapier.hbs
add|ember-template-lint|no-action|253|17|253|17|1907c9b1ae81cfb450b7a2ae7e0f874c26595144|1658102400000|1668474000000|1673658000000|app/templates/settings/integrations/zapier.hbs
add|ember-template-lint|no-action|254|15|254|15|20eb710122c95e74c9293fe14e11f105a86f172a|1658102400000|1668474000000|1673658000000|app/templates/settings/integrations/zapier.hbs
add|ember-template-lint|require-valid-alt-text|18|24|18|24|c1c041ff951875c8cf87ba24bd45d2a2a7597969|1658102400000|1668474000000|1673658000000|app/templates/settings/integrations/zapier.hbs
add|ember-template-lint|no-action|30|67|30|67|84cad828f9f84f35c077dfea0661dac1ccbbcc79|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/index.hbs
add|ember-template-lint|no-action|37|118|37|118|f00cc934ece7532a0c6ac89df3a05ff04b1f1501|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/index.hbs
add|ember-template-lint|no-action|44|19|44|19|40bf79016dee9fe1dca46383af6c4c35a5441ea9|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/index.hbs
add|ember-template-lint|no-action|50|19|50|19|de05e88a1b6b658430d78f36b93dfa372903c26d|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/index.hbs
add|ember-template-lint|no-action|89|103|89|103|8a5526f86c3c3289a47e2e033e39292ceaa3999e|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/index.hbs
add|ember-template-lint|no-action|92|105|92|105|8ef55f22a4332a43d47b2508c5903048f972cefa|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/index.hbs
add|ember-template-lint|no-route-action|63|62|63|62|e9f8663f45f4f41bc590f8db8df3d5dabd9cc732|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/index.hbs
add|ember-template-lint|no-action|22|25|22|25|df631bb317cb737790ce8fd9a9b107f5a196a10d|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-action|23|23|23|23|35178fb4b4e564116d2012fda7d3977a471fc8e7|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-action|39|54|39|54|5af61080eb82340db68e59c287a1e9273f805b1e|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-action|44|49|44|49|7f6812a5fb3d34d95b4e27ce4b9691b3065f7b8c|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-action|45|47|45|47|5af61080eb82340db68e59c287a1e9273f805b1e|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-action|52|93|52|93|a927cb3d62f088a149f64288c634921673563e7d|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-action|58|99|58|99|7628e64f5e59b4de2f529e029596f5570a4827ae|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-action|65|103|65|103|a98190ea4757d865b31ad00184c6cc12a89c1009|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-action|80|29|80|29|c81a8b61bc2a6a24c5103fd71843b71c96a7e413|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-action|81|27|81|27|a927cb3d62f088a149f64288c634921673563e7d|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-action|88|29|88|29|509c2ee1c3b1c57ee10b62649404561d5817de7f|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-action|89|27|89|27|7628e64f5e59b4de2f529e029596f5570a4827ae|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-action|99|31|99|31|a98190ea4757d865b31ad00184c6cc12a89c1009|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-action|104|33|104|33|34da22516e03547d6f322fe271a3251add8f3ec9|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-action|105|31|105|31|a98190ea4757d865b31ad00184c6cc12a89c1009|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-action|119|82|119|82|e2e24a0e1efe19ed982cc8140248570c03eee7b9|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-action|122|88|122|88|ea7c6e414cc202b6e4049eae4f60eb0829bd3ff0|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-action|126|35|126|35|ea7c6e414cc202b6e4049eae4f60eb0829bd3ff0|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-action|133|42|133|42|7a2a7a1b2159d811b310a9b89817108ddff9df88|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-action|137|35|137|35|7a2a7a1b2159d811b310a9b89817108ddff9df88|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-action|152|39|152|39|3da2270da5495c84c2b069564f2163b109d0e73e|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-action|153|43|153|43|4cd04fee6bbe85afe19501cd2f393c567e2f465e|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-action|172|39|172|39|66cebfc8448eced0bccf3faa57b4af0cd633e65e|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-action|173|43|173|43|3a3cdadac5b48ea65a6b10f0bb9319bed6d9ed79|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-action|193|43|193|43|eaee037b0dc6f3a8903f73c20098dfd77cba770c|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-action|194|47|194|47|b435e7cd7f48e3edb3214adc9c171181805500e3|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-action|207|75|207|75|e0b0429cdf24580adc31da56392cc9814a42b2bb|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-action|215|45|215|45|4a66eafd7ad8fea066c11d298665e40bb02e3fc1|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-action|216|43|216|43|923e950a7705bc29217afe2e0a81dbf6012777f1|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-action|227|39|227|39|5340ffcfe14bc0696c786cb698dc7436ff3e7b2a|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-action|228|43|228|43|cbc552ad67fd7382c02ac946a3de0fbe7dbb8509|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-action|243|39|243|39|6c405ec87c2cbc8470edc3bcba2ab93bf20e5c17|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-action|244|43|244|43|b26a1d1b4ba0bbe491099aeb78875160e2e417d6|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-action|259|39|259|39|1f32c13702a8b46410e9e10f9988fa2e0df41968|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-action|260|43|260|43|69b8d823159491abd67494eba728616ac0a928a5|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-action|276|39|276|39|226e659be4c2c10c0e5fbebb40f712f3016c9b90|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-action|277|43|277|43|d746c5532ce38d23d8537195a3afa6ce7235f577|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-action|289|39|289|39|61d6401bd1e43f082ab6fa49c83002d2e3faa886|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-action|290|43|290|43|eb4de7d8a9578a4b960da7d9b7679f3b1d67f886|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-action|332|106|332|106|d2028f094665a3702bbea21159b1c12237c94bc9|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-action|343|47|343|47|4661ca0fecfa23048521845c78a5a7cfce28609f|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-action|345|50|345|50|6403bc3b28507827968bd1fcd428d36d63b53511|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-action|360|43|360|43|c262ddbe8aa83b429a32392155869d7a068a5c3e|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-action|362|46|362|46|6403bc3b28507827968bd1fcd428d36d63b53511|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-action|375|43|375|43|08c167af90156f5bce0a15fe0153ba0c222297b6|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-action|377|46|377|46|6403bc3b28507827968bd1fcd428d36d63b53511|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-action|407|62|407|62|4150a56e60c88b9d8203a48c28069896522205ed|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-action|410|62|410|62|38bfdf3e4ec8c79d195e80a48c2f343757845cab|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-action|427|49|427|49|60dfff263d99797e3bcdf455c9c6ccf8ae539f1e|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-action|428|47|428|47|a1a5fab148882be2de9da5aca1d7d7e39e65a326|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-duplicate-landmark-elements|332|16|332|16|f8a398a428b26623555526f6e625aeca79d8dc72|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-duplicate-landmark-elements|393|16|393|16|c7e339eb9f5d83115a7fda2636d1487ba11b133d|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-invalid-interactive|207|62|207|62|19a5403007099acc29f4f7bbbb8fd008405002eb|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-passed-in-event-handlers|152|32|152|32|7f1d3292c273e623c95015be04a3fc5bac131480|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-passed-in-event-handlers|172|32|172|32|fed655605208b290a18b9f7da51a6cf7b40c0e9a|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-passed-in-event-handlers|193|36|193|36|881cf5dcdbe984cb7b4d5a2ddde4e111147b3817|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-passed-in-event-handlers|227|32|227|32|23912c3130690f1fb910732f42bc87922e3bc317|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-passed-in-event-handlers|243|32|243|32|50e244fdb1572202192f659fe4052f18bea92552|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-passed-in-event-handlers|259|32|259|32|c14e20b2b090706f9af9fa416c4300c06e206894|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-passed-in-event-handlers|276|32|276|32|e286f50c92a17a31581207e4c65054f146cbd533|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-passed-in-event-handlers|289|32|289|32|b6eefc591c761dcae13b0de50e074f5aac8f1d80|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-passed-in-event-handlers|343|40|343|40|f9ad8f1af9275247b94cbab1de26e9f9d2218da5|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-passed-in-event-handlers|360|36|360|36|2a5f6ae5ef152f0c0d953cfbaf810fdf0d08e996|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-passed-in-event-handlers|375|36|375|36|c9d092bb6707073e3f11bdc393d1c1361e5f87c4|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-potential-path-strings|404|53|404|53|5c3a010b4844cd530c312ef2aa32de64b8a43db1|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-valueless-arguments|402|44|402|44|5d042f1f94738e82cc1b0c5c008cece383beaa30|1658102400000|1668474000000|1673658000000|app/templates/settings/staff/user.hbs
add|ember-template-lint|no-down-event-binding|8|9|8|9|e82f6aa36fd44bb3dccff09770613eee19380f9b|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-alt-input.hbs
add|ember-template-lint|require-input-label|1|0|1|0|77a374cbfa52104125065d3ea3bb8ddee2e10a5e|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-alt-input.hbs
add|ember-template-lint|no-action|15|18|15|18|8e38f1d73c212f5fe08d155a9ba83f7dc30e38a8|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-basic-html-input.hbs
add|ember-template-lint|no-action|16|14|16|14|98c33bc8a7a032179cbbc279c04e635a4ac4bce3|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-basic-html-input.hbs
add|ember-template-lint|no-action|25|14|25|14|98c33bc8a7a032179cbbc279c04e635a4ac4bce3|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-basic-html-input.hbs
add|ember-template-lint|no-action|34|16|34|16|5b7ad6ecbae8b6379d42a3c7f343485cf7cecded|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-basic-html-input.hbs
add|ember-template-lint|no-action|15|18|15|18|8e38f1d73c212f5fe08d155a9ba83f7dc30e38a8|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-basic-html-textarea.hbs
add|ember-template-lint|no-action|16|14|16|14|98c33bc8a7a032179cbbc279c04e635a4ac4bce3|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-basic-html-textarea.hbs
add|ember-template-lint|no-action|25|14|25|14|98c33bc8a7a032179cbbc279c04e635a4ac4bce3|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-basic-html-textarea.hbs
add|ember-template-lint|no-action|34|16|34|16|5b7ad6ecbae8b6379d42a3c7f343485cf7cecded|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-basic-html-textarea.hbs
add|ember-template-lint|no-action|6|14|6|14|870ce151c8f27f58bc1f8abc4be7707d1790515e|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-caption-input.hbs
add|ember-template-lint|no-action|7|13|7|13|31c56395c52506a01b65825ef8bdb3b71a95c36b|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-caption-input.hbs
add|ember-template-lint|no-action|8|12|8|12|0dc0b984b33c3c1842d08f078270473b16371137|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-caption-input.hbs
add|ember-template-lint|no-action|9|15|9|15|3b95b78c2246816876653a854e700c00873186b1|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-caption-input.hbs
add|ember-template-lint|no-action|10|21|10|21|97ce84ec3c3e0aec9ad3d95414ffa3aa235d5ef3|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-caption-input.hbs
add|ember-template-lint|no-action|106|27|106|27|69d342e90e8b17545813ae908214a09cd8adbb4a|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-audio.hbs
add|ember-template-lint|no-invalid-interactive|160|29|160|29|97f6cf5d19825a64a958f2f80ed007ca2f6e0def|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-audio.hbs
add|ember-template-lint|no-passed-in-event-handlers|106|20|106|20|f6b81930532691e5917b7a365ea0286aca41c410|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-audio.hbs
add|ember-template-lint|require-input-label|115|20|115|20|6667d8ebc469e0d2ed3471a4b172410ce94e2895|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-audio.hbs
add|ember-template-lint|require-input-label|118|20|118|20|f3ae4f72a8db3c91f535113a7471dba2b0ba74c9|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-audio.hbs
add|ember-template-lint|require-valid-alt-text|59|32|59|32|67bd02b65f7f92dc7f0a23f1d0821d694c12822a|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-audio.hbs
add|ember-template-lint|require-valid-alt-text|71|32|71|32|f7cffe175337549e4def0097056f57a9f4590c33|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-audio.hbs
add|ember-template-lint|no-down-event-binding|62|25|62|25|a158be60fde14211f6abfbac329c94fa5a3b4a46|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-before-after.hbs
add|ember-template-lint|require-input-label|54|16|54|16|98e790dc74c88df1b5eb2b212149a2fb0f9cf83c|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-before-after.hbs
add|ember-template-lint|require-input-label|113|16|113|16|c610307bb78fba0a94b9f3f12f507cc37142f7b8|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-before-after.hbs
add|ember-template-lint|require-valid-alt-text|40|20|40|20|2376a21f7f520ed08dff9d123e45c14329d60537|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-before-after.hbs
add|ember-template-lint|require-valid-alt-text|48|20|48|20|ed3cb6ea286ecb0dcbf85a01fd434a8db45ba22d|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-before-after.hbs
add|ember-template-lint|require-valid-alt-text|69|24|69|24|fa6bca7cf3804e926c62628e15909bc57e409f83|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-before-after.hbs
add|ember-template-lint|require-valid-alt-text|93|24|93|24|3ac85fbe4dc6c41ea8f0ccbc5c52d31412fb6c7f|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-before-after.hbs
add|ember-template-lint|style-concatenation|34|64|34|64|51cf172bdfd49494cf9561032700824034048fa1|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-before-after.hbs
add|ember-template-lint|style-concatenation|38|20|38|20|e814b9aab6e7bf5d99c98e738bf7aa29b72d3bc2|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-before-after.hbs
add|ember-template-lint|style-concatenation|65|65|65|65|beec7c7fb1c31e93a99d5d353d4b1175f5238749|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-before-after.hbs
add|ember-template-lint|style-concatenation|121|61|121|61|beec7c7fb1c31e93a99d5d353d4b1175f5238749|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-before-after.hbs
add|ember-template-lint|no-action|5|16|5|16|822c6a91d06436c87443aba1c5316874b4de7e93|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-bookmark.hbs
add|ember-template-lint|no-action|6|18|6|18|40a9eaaf8ee3a30448bd8f5c51e4f57ad6f637cb|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-bookmark.hbs
add|ember-template-lint|no-action|7|16|7|16|b029d6288b77b183c28684a537f2054da87763b7|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-bookmark.hbs
add|ember-template-lint|no-action|8|14|8|14|45b936fe4ae520e86dc577dced7e4de626004fba|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-bookmark.hbs
add|ember-template-lint|no-action|53|28|53|28|ee695277e2f7dac9fbe5570ebfbe9c738280a976|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-bookmark.hbs
add|ember-template-lint|no-action|65|71|65|71|76726a13a086d82dab219df12e86db1773a9de32|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-bookmark.hbs
add|ember-template-lint|no-action|66|85|66|85|bb78ad59bc384ea0de5e9459da9d85f1735ce141|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-bookmark.hbs
add|ember-template-lint|no-action|67|38|67|38|3ad187464ff78253a0ea4dd17dcfcf0423f66864|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-bookmark.hbs
add|ember-template-lint|no-action|78|24|78|24|3c723798059c71aedfa95cd70507bc0308728544|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-bookmark.hbs
add|ember-template-lint|no-action|79|26|79|26|0dd30c3b295b8550ca0591cdf60cdc82dad4f868|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-bookmark.hbs
add|ember-template-lint|no-down-event-binding|79|16|79|16|911ca011b4e567dddb2df874008ecfacae248d2d|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-bookmark.hbs
add|ember-template-lint|require-input-label|72|12|72|12|f91038c1c6744efe51fa8a38debc4c012f4362b4|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-bookmark.hbs
add|ember-template-lint|require-valid-alt-text|30|36|30|36|c072441cc8793a1dca82328ed637368158b1aa7c|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-bookmark.hbs
add|ember-template-lint|require-valid-alt-text|42|32|42|32|08652917915a00a391c0881f39dca81d4f62a077|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-bookmark.hbs
add|ember-template-lint|no-unused-block-params|1|0|1|0|fa339a96dc0cc3f7b984951073db51aef92c09b3|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-button.hbs
add|ember-template-lint|no-invalid-interactive|64|160|64|160|645413ce9dfeb328b8259ae2209ac631aa30dc9d|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-callout.hbs
add|ember-template-lint|no-triple-curlies|81|20|81|20|5fc0b56a3c059cf57b60432ba413d490074e3315|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-callout.hbs
add|ember-template-lint|no-unused-block-params|1|0|1|0|8481ae69591188cc636eaae534ae8d328d95ef04|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-callout.hbs
add|ember-template-lint|no-triple-curlies|167|23|167|23|9109f8baf6d3a4cce8e0ceb13f5bca99bdbe4004|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-email-cta.hbs
add|ember-template-lint|no-unused-block-params|1|0|1|0|08ed50a248b697129b713403bcaf9549b027bd11|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-email-cta.hbs
add|ember-template-lint|no-action|9|16|9|16|822c6a91d06436c87443aba1c5316874b4de7e93|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-email.hbs
add|ember-template-lint|no-action|10|18|10|18|40a9eaaf8ee3a30448bd8f5c51e4f57ad6f637cb|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-email.hbs
add|ember-template-lint|no-action|11|14|11|14|45b936fe4ae520e86dc577dced7e4de626004fba|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-email.hbs
add|ember-template-lint|no-action|12|14|12|14|c27072dba50f6e36c62d708160adb2e96b8244e3|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-email.hbs
add|ember-template-lint|no-action|14|17|14|17|b562d5c8ffc834037fff995329aa4cd49218776b|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-email.hbs
add|ember-template-lint|no-action|26|22|26|22|5fef4f84e130c50100035200a6996e594f8c4a9a|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-email.hbs
add|ember-template-lint|no-action|27|21|27|21|31c56395c52506a01b65825ef8bdb3b71a95c36b|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-email.hbs
add|ember-template-lint|no-action|28|20|28|20|0dc0b984b33c3c1842d08f078270473b16371137|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-email.hbs
add|ember-template-lint|no-action|29|29|29|29|97ce84ec3c3e0aec9ad3d95414ffa3aa235d5ef3|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-email.hbs
add|ember-template-lint|no-triple-curlies|38|11|38|11|9109f8baf6d3a4cce8e0ceb13f5bca99bdbe4004|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-email.hbs
add|ember-template-lint|no-action|5|16|5|16|822c6a91d06436c87443aba1c5316874b4de7e93|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-embed.hbs
add|ember-template-lint|no-action|6|18|6|18|40a9eaaf8ee3a30448bd8f5c51e4f57ad6f637cb|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-embed.hbs
add|ember-template-lint|no-action|7|16|7|16|b029d6288b77b183c28684a537f2054da87763b7|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-embed.hbs
add|ember-template-lint|no-action|8|14|8|14|45b936fe4ae520e86dc577dced7e4de626004fba|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-embed.hbs
add|ember-template-lint|no-action|33|28|33|28|ee695277e2f7dac9fbe5570ebfbe9c738280a976|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-embed.hbs
add|ember-template-lint|no-action|46|71|46|71|76726a13a086d82dab219df12e86db1773a9de32|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-embed.hbs
add|ember-template-lint|no-action|47|85|47|85|bb78ad59bc384ea0de5e9459da9d85f1735ce141|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-embed.hbs
add|ember-template-lint|no-action|48|38|48|38|3ad187464ff78253a0ea4dd17dcfcf0423f66864|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-embed.hbs
add|ember-template-lint|no-action|59|24|59|24|3c723798059c71aedfa95cd70507bc0308728544|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-embed.hbs
add|ember-template-lint|no-action|60|26|60|26|0dd30c3b295b8550ca0591cdf60cdc82dad4f868|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-embed.hbs
add|ember-template-lint|no-down-event-binding|60|16|60|16|911ca011b4e567dddb2df874008ecfacae248d2d|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-embed.hbs
add|ember-template-lint|require-iframe-title|25|20|25|20|f8c77eef3da5ea547d640216997d9ac18f0c71d9|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-embed.hbs
add|ember-template-lint|require-input-label|53|12|53|12|f5eb68e50039dcad089ce49b17e85a32d7ebe2cf|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-embed.hbs
add|ember-template-lint|no-action|38|31|38|31|47a65e4376498373f3974d66dc59bb74d0528bb0|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-file.hbs
add|ember-template-lint|no-action|44|31|44|31|06b0c88729efcb3c00abca78472bc02f561ecccd|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-file.hbs
add|ember-template-lint|no-invalid-interactive|92|29|92|29|ae3be0caab4a081a92fdd880dc74321cd6d780e0|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-file.hbs
add|ember-template-lint|no-passed-in-event-handlers|38|24|38|24|046ed81ef8af3995371ed6258c24c8df23a9d064|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-file.hbs
add|ember-template-lint|no-passed-in-event-handlers|44|24|44|24|675ef008ec0d64643d6a6b1bc6df36bc23709102|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-file.hbs
add|ember-template-lint|no-unused-block-params|1|0|1|0|156821bab3e9e1e1fe662b997173fa21ba6ec8ff|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-file.hbs
add|ember-template-lint|no-action|6|16|6|16|822c6a91d06436c87443aba1c5316874b4de7e93|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-gallery.hbs
add|ember-template-lint|no-action|7|18|7|18|40a9eaaf8ee3a30448bd8f5c51e4f57ad6f637cb|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-gallery.hbs
add|ember-template-lint|no-action|8|14|8|14|45b936fe4ae520e86dc577dced7e4de626004fba|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-gallery.hbs
add|ember-template-lint|no-action|16|14|16|14|d7c2ceb3a1005e005da2fed8a565d2ce929af5a5|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-gallery.hbs
add|ember-template-lint|no-action|17|16|17|16|df8466847c2a74b5331ee6768c4737c571712edd|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-gallery.hbs
add|ember-template-lint|no-action|24|23|24|23|79ce5098a6bd76f352070aea4492093460743b0b|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-gallery.hbs
add|ember-template-lint|no-action|25|25|25|25|6a4072f62408f1964b1977ce4462b1d61537b548|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-gallery.hbs
add|ember-template-lint|no-action|26|25|26|25|b74e8f2d4504aa944639bd7a413292fae5cb209a|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-gallery.hbs
add|ember-template-lint|no-action|27|18|27|18|bd362a31233c52421c9db737d3d10b42ddefd6bd|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-gallery.hbs
add|ember-template-lint|no-action|50|110|50|110|ede1c2002a22780dcb2e86e6b07196b87f509a40|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-gallery.hbs
add|ember-template-lint|no-action|72|143|72|143|69e5f19c951bf70e9e7eaa72ea2b784ab97c3dff|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-gallery.hbs
add|ember-template-lint|no-action|90|40|90|40|8b347765bd019e65941a73b8132dbd3cdcf3fad5|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-gallery.hbs
add|ember-template-lint|no-action|99|52|99|52|76e4cca6ff2f817714404678f70aca9e64f58f7f|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-gallery.hbs
add|ember-template-lint|no-action|106|20|106|20|ee695277e2f7dac9fbe5570ebfbe9c738280a976|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-gallery.hbs
add|ember-template-lint|require-valid-alt-text|41|36|41|36|c32a307c8857ccf119696235e8fe2858687dbfa2|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-gallery.hbs
add|ember-template-lint|no-action|31|26|31|26|520ad6b38441a28ec45d4bd8daaccf36ee50614c|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-header.hbs
add|ember-template-lint|no-action|32|33|32|33|fc61d9c3c77f842c8389d4abb63e7a467a596c40|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-header.hbs
add|ember-template-lint|no-action|40|26|40|26|eac3d5c693a72de84fab7b4dc2b3956588248d1e|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-header.hbs
add|ember-template-lint|no-action|41|33|41|33|7352d51cbb767ffbdc8fd829b0013a0ca2d0c809|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-header.hbs
add|ember-template-lint|no-invalid-interactive|91|97|91|97|38e55d655123bc526549b4920480a65835f9890b|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-header.hbs
add|ember-template-lint|no-triple-curlies|172|46|172|46|f85a33dff9dc1a580ba55ca8e6f73bf1cd671f69|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-header.hbs
add|ember-template-lint|no-triple-curlies|174|53|174|53|8aa6418d946a4a6de4a9f7e963860014b0dd68ee|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-header.hbs
add|ember-template-lint|no-unused-block-params|1|0|1|0|e25f7866ab4ee682b08edf3b29a1351e4079538e|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-header.hbs
add|ember-template-lint|require-valid-alt-text|91|32|91|32|38e55d655123bc526549b4920480a65835f9890b|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-header.hbs
add|ember-template-lint|style-concatenation|24|90|24|90|17cca11a71b4921b82aa55b4d5772e6801e7d001|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-header.hbs
add|ember-template-lint|style-concatenation|171|90|171|90|17cca11a71b4921b82aa55b4d5772e6801e7d001|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-header.hbs
add|ember-template-lint|no-action|9|16|9|16|822c6a91d06436c87443aba1c5316874b4de7e93|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-html.hbs
add|ember-template-lint|no-action|10|18|10|18|40a9eaaf8ee3a30448bd8f5c51e4f57ad6f637cb|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-html.hbs
add|ember-template-lint|no-action|11|14|11|14|45b936fe4ae520e86dc577dced7e4de626004fba|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-html.hbs
add|ember-template-lint|no-action|12|14|12|14|c27072dba50f6e36c62d708160adb2e96b8244e3|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-html.hbs
add|ember-template-lint|no-action|14|17|14|17|b562d5c8ffc834037fff995329aa4cd49218776b|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-html.hbs
add|ember-template-lint|no-action|22|20|22|20|5fef4f84e130c50100035200a6996e594f8c4a9a|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-html.hbs
add|ember-template-lint|no-quoteless-attributes|20|23|20|23|20c117eaeeb511ade7f0ac922eb5dd4fa8e226df|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-html.hbs
add|ember-template-lint|no-quoteless-attributes|21|26|21|26|20c117eaeeb511ade7f0ac922eb5dd4fa8e226df|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-html.hbs
add|ember-template-lint|no-triple-curlies|25|47|25|47|8336bb2ae349079452ea98bf142c3e7dd649c549|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-html.hbs
add|ember-template-lint|no-action|7|16|7|16|822c6a91d06436c87443aba1c5316874b4de7e93|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-image.hbs
add|ember-template-lint|no-action|8|18|8|18|40a9eaaf8ee3a30448bd8f5c51e4f57ad6f637cb|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-image.hbs
add|ember-template-lint|no-action|9|14|9|14|45b936fe4ae520e86dc577dced7e4de626004fba|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-image.hbs
add|ember-template-lint|no-action|13|16|13|16|b029d6288b77b183c28684a537f2054da87763b7|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-image.hbs
add|ember-template-lint|no-action|24|19|24|19|79b97ed19b0c3220eeee237e671cc380b8b10d6f|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-image.hbs
add|ember-template-lint|no-action|25|18|25|18|267fef7268487857463625638157e369c3ad9344|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-image.hbs
add|ember-template-lint|no-action|32|21|32|21|51bd4f460e5c86c46ccecbcb52638a43f8ca9b07|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-image.hbs
add|ember-template-lint|no-action|33|24|33|24|6c35771c823fe159562109db4dbe1dec3cfe82f0|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-image.hbs
add|ember-template-lint|no-action|34|22|34|22|0791eeb5a6fa727f27b18b9cbba690b3e7633908|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-image.hbs
add|ember-template-lint|no-action|64|150|64|150|69e5f19c951bf70e9e7eaa72ea2b784ab97c3dff|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-image.hbs
add|ember-template-lint|no-action|105|19|105|19|79b97ed19b0c3220eeee237e671cc380b8b10d6f|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-image.hbs
add|ember-template-lint|no-action|106|18|106|18|267fef7268487857463625638157e369c3ad9344|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-image.hbs
add|ember-template-lint|no-action|115|16|115|16|7ce20fdf2216b0a53e1954f99a03972382f14635|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-image.hbs
add|ember-template-lint|no-action|116|16|116|16|5b7ad6ecbae8b6379d42a3c7f343485cf7cecded|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-image.hbs
add|ember-template-lint|simple-unless|37|41|37|41|ca177565b6e1c5a6175982047708732f5dde7e59|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-image.hbs
add|ember-template-lint|no-action|9|17|9|17|2ecb8ec2db47e693d292cac348977b4c83e5b0a4|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-markdown.hbs
add|ember-template-lint|no-action|10|17|10|17|b562d5c8ffc834037fff995329aa4cd49218776b|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-markdown.hbs
add|ember-template-lint|no-action|11|16|11|16|822c6a91d06436c87443aba1c5316874b4de7e93|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-markdown.hbs
add|ember-template-lint|no-action|12|18|12|18|40a9eaaf8ee3a30448bd8f5c51e4f57ad6f637cb|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-markdown.hbs
add|ember-template-lint|no-action|13|14|13|14|45b936fe4ae520e86dc577dced7e4de626004fba|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-markdown.hbs
add|ember-template-lint|no-action|14|14|14|14|c27072dba50f6e36c62d708160adb2e96b8244e3|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-markdown.hbs
add|ember-template-lint|no-action|22|23|22|23|9ab4bf8e456620319065ed4f13de2d91f7acfb2c|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-markdown.hbs
add|ember-template-lint|no-action|23|22|23|22|d421a73a0ba7c7c037ba76cf8d94e18ccc428ad3|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-markdown.hbs
add|ember-template-lint|no-action|24|33|24|33|a7539f813f4a3b95a771a7a8f372d651c5352ba2|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-markdown.hbs
add|ember-template-lint|no-action|29|26|29|26|98bcc5096c8ee474d35bace3f1987716a5cb0901|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-markdown.hbs
add|ember-template-lint|no-action|36|38|36|38|79b18b69be48cb15fe020b57db44cb55f0d54cc3|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-markdown.hbs
add|ember-template-lint|no-action|44|23|44|23|3a6fe226433a067ecc30866b08c5e55e6bb93059|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-markdown.hbs
add|ember-template-lint|no-action|45|22|45|22|3f794b4f3e5a21abbe19fbf15aba586928af4f54|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-markdown.hbs
add|ember-template-lint|no-action|46|33|46|33|a2d6115b0418698f5ccb097023a3039b9c0e8dfa|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-markdown.hbs
add|ember-template-lint|no-action|64|32|64|32|fcd38782431c3a033d606fe06875297ad0fbf5a8|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-markdown.hbs
add|ember-template-lint|no-action|65|30|65|30|d819379a4dcb9a1425d0998fa83c0b738b257832|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-markdown.hbs
add|ember-template-lint|no-action|77|93|77|93|e64e7ee5049e28719f11468dfc3d25a51cbd751c|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-markdown.hbs
add|ember-template-lint|no-action|42|193|42|193|0791eeb5a6fa727f27b18b9cbba690b3e7633908|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-product.hbs
add|ember-template-lint|no-action|71|114|71|114|69e5f19c951bf70e9e7eaa72ea2b784ab97c3dff|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-product.hbs
add|ember-template-lint|no-action|35|30|35|30|ea98008468a2546cc4db5918175d339c105557e7|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-toggle.hbs
add|ember-template-lint|no-action|36|29|36|29|31c56395c52506a01b65825ef8bdb3b71a95c36b|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-toggle.hbs
add|ember-template-lint|no-action|37|28|37|28|0dc0b984b33c3c1842d08f078270473b16371137|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-toggle.hbs
add|ember-template-lint|no-action|50|30|50|30|2568a6bbfc0f9b98fc443036900050aecbe849a1|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-toggle.hbs
add|ember-template-lint|no-action|51|29|51|29|31c56395c52506a01b65825ef8bdb3b71a95c36b|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-toggle.hbs
add|ember-template-lint|no-action|52|28|52|28|0dc0b984b33c3c1842d08f078270473b16371137|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-toggle.hbs
add|ember-template-lint|no-action|53|37|53|37|89578bd62979ce1da14325c918197f524cd73d6b|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-toggle.hbs
add|ember-template-lint|no-triple-curlies|63|24|63|24|b4fdf1937520db3f14f6f93ec84f03b7653414c0|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-toggle.hbs
add|ember-template-lint|no-triple-curlies|72|16|72|16|9197e9050977ef47965e1b6e8d99bee542a7c559|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-toggle.hbs
add|ember-template-lint|no-unused-block-params|1|0|1|0|c81c511a566ffd14b5570b2a72d89edb5ec390a0|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-toggle.hbs
add|ember-template-lint|no-invalid-interactive|153|105|153|105|3a10a7d94665ddc129c8ad4d64d50a4d9b5c9e34|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-video.hbs
add|ember-template-lint|require-input-label|84|24|84|24|6667d8ebc469e0d2ed3471a4b172410ce94e2895|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-video.hbs
add|ember-template-lint|require-input-label|87|24|87|24|f3ae4f72a8db3c91f535113a7471dba2b0ba74c9|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-video.hbs
add|ember-template-lint|require-valid-alt-text|39|20|39|20|26bfc8a4d99f4dd6f6340dc759411bc91c36ef9f|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-video.hbs
add|ember-template-lint|require-valid-alt-text|153|40|153|40|3a10a7d94665ddc129c8ad4d64d50a4d9b5c9e34|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-video.hbs
add|ember-template-lint|simple-unless|36|37|36|37|ca177565b6e1c5a6175982047708732f5dde7e59|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-video.hbs
add|ember-template-lint|simple-unless|41|26|41|26|ca177565b6e1c5a6175982047708732f5dde7e59|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-video.hbs
add|ember-template-lint|no-action|36|45|36|45|44bab019144785f1c79a5928943790ae6c687106|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card.hbs
add|ember-template-lint|no-down-event-binding|36|33|36|33|a158be60fde14211f6abfbac329c94fa5a3b4a46|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card.hbs
add|ember-template-lint|style-concatenation|2|24|2|24|2d0ef0aab274628106cd50bf3f1f51a54a0de24b|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card.hbs
add|ember-template-lint|style-concatenation|5|9|5|9|3237d3db72009ef7baa5ebfa8d525fa5fd81262c|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card.hbs
add|ember-template-lint|no-action|11|18|11|18|8e38f1d73c212f5fe08d155a9ba83f7dc30e38a8|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-editor.hbs
add|ember-template-lint|no-action|12|19|12|19|7ca45569cf222ba15db12d11063b92224038d2e3|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-editor.hbs
add|ember-template-lint|no-action|13|25|13|25|8b1ee860eec1b61ced23c00e743046d9cad53a33|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-editor.hbs
add|ember-template-lint|no-action|14|14|14|14|98c33bc8a7a032179cbbc279c04e635a4ac4bce3|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-editor.hbs
add|ember-template-lint|no-action|24|14|24|14|98c33bc8a7a032179cbbc279c04e635a4ac4bce3|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-editor.hbs
add|ember-template-lint|no-action|34|16|34|16|5b7ad6ecbae8b6379d42a3c7f343485cf7cecded|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-editor.hbs
add|ember-template-lint|no-action|56|32|56|32|6ef6aab7e798fbacc42ce92a4f56b747f8037908|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-editor.hbs
add|ember-template-lint|no-action|58|15|58|15|76f3c0a0e763a852707dbd87fcc01fba0bea5e4f|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-editor.hbs
add|ember-template-lint|no-action|71|28|71|28|6ef6aab7e798fbacc42ce92a4f56b747f8037908|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-editor.hbs
add|ember-template-lint|no-action|72|21|72|21|dc1cf61f19b8aed1edcea87cfc8b968c229f9c93|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-editor.hbs
add|ember-template-lint|no-action|73|27|73|27|9195baebc5cc53f4be05b0aa955ab8e22a3dd099|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-editor.hbs
add|ember-template-lint|no-action|83|28|83|28|6ef6aab7e798fbacc42ce92a4f56b747f8037908|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-editor.hbs
add|ember-template-lint|no-action|84|21|84|21|dc1cf61f19b8aed1edcea87cfc8b968c229f9c93|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-editor.hbs
add|ember-template-lint|no-action|85|27|85|27|9195baebc5cc53f4be05b0aa955ab8e22a3dd099|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-editor.hbs
add|ember-template-lint|no-action|100|21|100|21|8cda060f630c81587747ae63bca22ba0479a417d|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-editor.hbs
add|ember-template-lint|no-action|101|23|101|23|bbab5cd293cf71a2af917d6f84d566c42ff313b1|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-editor.hbs
add|ember-template-lint|no-action|102|23|102|23|071fd2886b454eafc96ec0cc9a7ef01503a2688d|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-editor.hbs
add|ember-template-lint|no-action|103|25|103|25|7db56e6734f46e63e91445e399e1a3a602c31636|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-editor.hbs
add|ember-template-lint|no-action|106|23|106|23|0401c18c6ce3fe2477e645d5923710feb6491de3|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-editor.hbs
add|ember-template-lint|no-action|107|25|107|25|74128407cfa61ff0257028d0266c841fdebcdcb1|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-editor.hbs
add|ember-template-lint|no-action|108|21|108|21|24b49ddef48d36b91530840b7831b973e37465b8|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-editor.hbs
add|ember-template-lint|no-action|109|23|109|23|61683de5dc0f30ebbc34e67eba5fc99df939a892|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-editor.hbs
add|ember-template-lint|no-action|111|36|111|36|bdce7d080c258a9867f07eb0e47da8248ae2f1ef|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-editor.hbs
add|ember-template-lint|no-action|112|36|112|36|faef61191a8b49a36a9c0951e74d2a041f9e2912|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-editor.hbs
add|ember-template-lint|no-action|113|34|113|34|4438907afa3f75452364ec151c6f926829541538|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-editor.hbs
add|ember-template-lint|no-action|114|30|114|30|5e9930951aa23775756479da419025121c272239|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-editor.hbs
add|ember-template-lint|no-action|126|30|126|30|5cd4dd9d1fe33c1ecd959992079dda2516b1f4f5|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-editor.hbs
add|ember-template-lint|no-action|5|17|5|17|4db7a2d3c266d607f2440d7dc4d64625c94f7298|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-link-input.hbs
add|ember-template-lint|no-action|6|19|6|19|18a04ff082ca003ee43fe89db6e45bbef426200d|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-link-input.hbs
add|ember-template-lint|no-action|10|85|10|85|fdbf1af52da481e21da206e8ea385c625555f753|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-link-input.hbs
add|ember-template-lint|no-down-event-binding|6|9|6|9|e82f6aa36fd44bb3dccff09770613eee19380f9b|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-link-input.hbs
add|ember-template-lint|require-input-label|1|0|1|0|8e9e65247c8b4d7f421d2280af07a9ff39a8a501|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-link-input.hbs
add|ember-template-lint|no-action|11|16|11|16|581ab008eb88e1bc0a0900599fc2b22da7bd0501|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-link-toolbar.hbs
add|ember-template-lint|no-action|22|16|22|16|44aef00d7b12f6b9128d3e6f8debe3d2d7b3194d|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-link-toolbar.hbs
add|ember-template-lint|no-args-paths|4|14|4|14|9ba62e613990ee354745eb5b05acb24e54e55302|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-media-selector.hbs
add|ember-template-lint|no-invalid-interactive|23|66|23|66|12fb0d3e1b679f781b065226f36d8afdcd5cedf0|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-menu-content.hbs
add|ember-template-lint|simple-unless|4|79|4|79|5d9203e10703908836b537481cf012f167ded239|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-menu-content.hbs
add|ember-template-lint|no-action|2|174|2|174|c69c5c97cc0db96f1040900aa3885fa3da5be7c2|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-plus-menu.hbs
add|ember-template-lint|no-action|14|25|14|25|dcfe39b0f6e42f3e4cb5138223798067d7a0a2b7|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-plus-menu.hbs
add|ember-template-lint|no-action|15|18|15|18|8e38f1d73c212f5fe08d155a9ba83f7dc30e38a8|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-text-replacement-html-input.hbs
add|ember-template-lint|no-action|16|14|16|14|98c33bc8a7a032179cbbc279c04e635a4ac4bce3|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-text-replacement-html-input.hbs
add|ember-template-lint|no-action|25|14|25|14|98c33bc8a7a032179cbbc279c04e635a4ac4bce3|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-text-replacement-html-input.hbs
add|ember-template-lint|no-action|34|16|34|16|5b7ad6ecbae8b6379d42a3c7f343485cf7cecded|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-text-replacement-html-input.hbs
add|ember-template-lint|no-action|3|13|3|13|288404c640071968cb22e09f4b5f2e815e0f03ea|1658102400000|1668474000000|1673658000000|app/templates/settings/integration/webhooks/edit.hbs
add|ember-template-lint|no-action|4|11|4|11|831dd12209c22868d0614c65417281fda7991f09|1658102400000|1668474000000|1673658000000|app/templates/settings/integration/webhooks/edit.hbs
add|ember-template-lint|no-action|3|13|3|13|288404c640071968cb22e09f4b5f2e815e0f03ea|1658102400000|1668474000000|1673658000000|app/templates/settings/integration/webhooks/new.hbs
add|ember-template-lint|no-action|4|11|4|11|831dd12209c22868d0614c65417281fda7991f09|1658102400000|1668474000000|1673658000000|app/templates/settings/integration/webhooks/new.hbs
add|ember-template-lint|require-valid-alt-text|3|44|3|44|a7f0566c430150bae4153e0dfb489a218bdeb8a4|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-embed/nft.hbs
add|ember-template-lint|require-valid-alt-text|8|20|8|20|9d0c591086dc9139ff38a7b385c3367a83438786|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-embed/nft.hbs
add|ember-template-lint|require-input-label|10|12|10|12|8c3c0ea315ff4da828363989a45fa11256a78796|1658102400000|1668474000000|1673658000000|lib/koenig-editor/addon/components/koenig-card-image/selector-tenor.hbs
remove|ember-template-lint|no-down-event-binding|5|13|5|13|a158be60fde14211f6abfbac329c94fa5a3b4a46|1658102400000|1668474000000|1673658000000|app/components/gh-koenig-editor.hbs
remove|ember-template-lint|no-invalid-interactive|5|8|5|8|94046126dd697b080aa16222b00a3d5a545ee001|1658102400000|1668474000000|1673658000000|app/components/gh-koenig-editor.hbs
remove|ember-template-lint|no-invalid-interactive|6|8|6|8|94046126dd697b080aa16222b00a3d5a545ee001|1658102400000|1668474000000|1673658000000|app/components/gh-koenig-editor.hbs
remove|ember-template-lint|no-passed-in-event-handlers|26|12|26|12|4ad1176b9a4bb23d79114735b478240166113502|1658102400000|1668474000000|1673658000000|app/components/gh-koenig-editor.hbs
remove|ember-template-lint|no-passed-in-event-handlers|28|12|28|12|7ffe0a55c81efcfcd46880a930fa69bee73835b3|1658102400000|1668474000000|1673658000000|app/components/gh-koenig-editor.hbs
remove|ember-template-lint|no-whitespace-within-word|80|20|80|20|5ec988f82d06bb76be61f23dbb9cb65f94326f34|1658102400000|1668474000000|1673658000000|app/components/modal-markdown-help.hbs

View File

@ -0,0 +1,8 @@
module.exports = {
'ember-template-lint': {
daysToDecay: {
warn: 120,
error: 180,
}
}
};

View File

@ -0,0 +1,9 @@
module.exports = {
extends: "recommended",
rules: {
'no-forbidden-elements': ['meta', 'html', 'script'],
'no-implicit-this': {allow: ['noop', 'now', 'site-icon-style', 'accent-color-background']},
'no-inline-styles': false
}
};

View File

@ -0,0 +1,3 @@
{
"ignore_dirs": ["tmp", "dist"]
}

70
ghost/admin/Gruntfile.js Normal file
View File

@ -0,0 +1,70 @@
/* eslint-env node */
/* eslint-disable object-shorthand */
'use strict';
module.exports = function (grunt) {
// Find all of the task which start with `grunt-` and load them, rather than explicitly declaring them all
require('matchdep').filterDev(['grunt-*', '!grunt-cli']).forEach(grunt.loadNpmTasks);
grunt.initConfig({
clean: {
built: {
src: ['dist/**']
},
dependencies: {
src: ['node_modules/**']
},
tmp: {
src: ['tmp/**']
}
},
watch: {
csscomb: {
files: ['app/styles/**/*.css'],
tasks: ['shell:csscombfix']
}
},
shell: {
'npm-install': {
command: 'yarn install'
},
ember: {
command: function (mode) {
let liveReloadBaseUrl = grunt.option('live-reload-base-url') || '/ghost/';
switch (mode) {
case 'prod':
return 'npm run build -- --environment=production --silent';
case 'dev':
return 'npm run build';
case 'watch':
return `npm run start -- --live-reload-base-url=${liveReloadBaseUrl} --live-reload-port=4201`;
}
}
},
csscombfix: {
command: 'csscomb -c app/styles/csscomb.json -v app/styles'
},
csscomblint: {
command: 'csscomb -c app/styles/csscomb.json -lv app/styles'
},
test: {
command: 'npm test'
},
options: {
preferLocal: true
}
}
});
grunt.registerTask('init', 'Install the admin dependencies',
['shell:npm-install']
);
};

22
ghost/admin/LICENSE Normal file
View File

@ -0,0 +1,22 @@
Copyright (c) 2013-2022 Ghost Foundation
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

68
ghost/admin/README.md Normal file
View File

@ -0,0 +1,68 @@
# Ghost-Admin
![](https://github.com/TryGhost/Admin/workflows/Test%20Suite/badge.svg?branch=main)
This is the home of Ember.js based admin app that ships with [Ghost](https://github.com/tryghost/ghost).
**Do you want to set up a Ghost blog?** Check the [getting started guide](https://ghost.org/docs/introduction/)
**Do you want to modify or contribute to Ghost-Admin?** Please read how to [install from source](https://ghost.org/docs/install/source/) and swing by our [forum](https://forum.ghost.org) if you need any help 😄
## Running tests
Build and run tests once:
```bash
TZ=UTC yarn test
```
_Note the `TZ=UTC` environment variable which is currently required to get tests working if your system timezone doesn't match UTC._
If you are serving the admin app (e.g., when running `yarn serve`, or when running `yarn dev` in the main Ghost project), you can also run the tests in your browser by going to http://localhost:4200/tests.
This has the additional benefit that you can use `await this.pauseTest()` in your tests to temporarily pause tests (best to also add `this.timeout(0);` to avoid timeouts). This allows you to inspect the DOM in your browser to debug tests. You can resume tests by running `resumeTest()` in your browser console.
[More information](https://guides.emberjs.com/v3.28.0/testing/testing-application/#toc_debugging-your-tests)
### Writing tests
When writing tests and not using the `http://localhost:4200/tests` browser tests, it can be easier to have a separate watching build that builds the project for the test environment (this drastically reduces the time you have to wait when running tests):
```bash
yarn build --environment=test -w -o="dist-test"
```
After that, you can easily run tests locally:
Run all tests:
```bash
TZ=UTC yarn test 1 --path="dist-test"
```
To have a cleaner output:
```bash
TZ=UTC yarn test 1 --reporter dot --path="dist-test"
```
This shows a dot (`.`) for every successful test, and `F` for every failed test. At the end, it will only show the output of the failed tests.
To run a specific test file:
```bash
TZ=UTC yarn test 1 --reporter dot --path="dist-test" -mp=tests/acceptance/settings/newsletters-test.js
```
_Hint: you can easily copy the path of a test in VSCode by right clicking on the test file and choosing `Copy Relative Path`._
To have a full list of the available options, run
```bash
ember exam --help
```
## Have a bug or issue?
Bugs and issues (even if they only affect the admin app) should be opened on the core [Ghost](https://github.com/tryghost/ghost/issues) repository.
# Copyright & License
Copyright (c) 2013-2022 Ghost Foundation - Released under the [MIT license](LICENSE). Ghost and the Ghost Logo are trademarks of Ghost Foundation Ltd. Please see our [trademark policy](https://ghost.org/trademark/) for info on acceptable usage.

9
ghost/admin/SECURITY.md Normal file
View File

@ -0,0 +1,9 @@
# Reporting Security Vulnerabilities
Potential security vulnerabilities can be reported directly us at `security@ghost.org`. The Ghost Security Team communicates privately and works in a secured, isolated repository for tracking, testing, and resolving security-related issues.
The full, up-to-date details of our security policy and procedure can always be found in our documentation:
https://ghost.org/docs/security/
Please refer to this before emailing us. Thanks for helping make Ghost safe for everyone 🙏.

30
ghost/admin/app/README.md Normal file
View File

@ -0,0 +1,30 @@
# Ghost Admin App
Ember.js application used as a client-side admin for the [Ghost](http://ghost.org) blogging platform. This readme is a work in progress guide aimed at explaining the specific nuances of the Ghost Ember app to contributors whose main focus is on this side of things.
## CSS
We use pure CSS, which is pre-processed for backwards compatibility by [Myth](http://myth.io). We do not follow any strict CSS framework, however our general style is pretty similar to BEM.
Styles are primarily broken up into 4 main categories:
* **Patterns** - are base level visual styles for HTML elements (eg. Buttons)
* **Components** - are groups of patterns used to create a UI component (eg. Modals)
* **Layouts** - are groups of components used to create application screens (eg. Settings)
All of these separate files are subsequently imported and compiled in `app.css`.
## Front End Standards
* 4 spaces for HTML & CSS indentation. Never tabs.
* Double quotes only, never single quotes.
* Use tags and elements appropriate for an HTML5 doctype (including self-closing tags)
* Adhere to the [Recess CSS](http://markdotto.com/2011/11/29/css-property-order/) property order.
* Always a space after a property's colon (.e.g, display: block; and not display:block;).
* End all lines with a semi-colon.
* For multiple, comma-separated selectors, place each selector on its own line.
* Use js- prefixed classes for JavaScript hooks into the DOM, and never use these in CSS as per [Slightly Obtrusive JavaSript](http://ozmm.org/posts/slightly_obtrusive_javascript.html)
* Avoid over-nesting CSS. Never nest more than 3 levels deep.
* Use comments to explain "why" not "what" (Good: This requires a z-index in order to appear above mobile navigation. Bad: This is a thing which is always on top!)

View File

@ -0,0 +1,16 @@
import ApplicationAdapter from './application';
import classic from 'ember-classic-decorator';
@classic
export default class ApiKey extends ApplicationAdapter {
queryRecord(store, type, query) {
if (!query || query.id !== 'me') {
return super.queryRecord(...arguments);
}
let url = `${this.buildURL('users', 'me')}token/`;
return this.ajax(url, 'GET', {data: {}}).then((data) => {
return data;
});
}
}

View File

@ -0,0 +1,9 @@
import EmbeddedRelationAdapter from 'ghost-admin/adapters/embedded-relation-adapter';
import classic from 'ember-classic-decorator';
@classic
export default class Application extends EmbeddedRelationAdapter {
shouldBackgroundReloadRecord() {
return false;
}
}

View File

@ -0,0 +1,44 @@
import AjaxServiceSupport from 'ember-ajax/mixins/ajax-support';
import RESTAdapter from '@ember-data/adapter/rest';
import ghostPaths from 'ghost-admin/utils/ghost-paths';
import {inject as service} from '@ember/service';
import {underscore} from '@ember/string';
export default RESTAdapter.extend(AjaxServiceSupport, {
host: window.location.origin,
namespace: ghostPaths().apiRoot.slice(1),
session: service(),
shouldBackgroundReloadRecord() {
return false;
},
query(store, type, query) {
let id;
if (query.id) {
id = query.id;
delete query.id;
}
return this.ajax(this.buildURL(type.modelName, id), 'GET', {data: query});
},
pathForType() {
const type = this._super(...arguments);
return underscore(type);
},
buildURL() {
// Ensure trailing slashes
let url = this._super(...arguments);
let parsedUrl = new URL(url);
if (!parsedUrl.pathname.endsWith('/')) {
parsedUrl.pathname += '/';
}
return parsedUrl.toString();
}
});

View File

@ -0,0 +1,15 @@
import ApplicationAdapter from 'ghost-admin/adapters/application';
export default class CustomThemeSettingListAdapter extends ApplicationAdapter {
// we use `custom-theme-setting-list` model as a workaround for saving all
// custom theme setting records in one request so it uses the base model url
pathForType() {
return 'custom_theme_settings';
}
// there's no custom theme setting creation
// newListModel.save() acts as an overall update request so force a PUT
createRecord(store, type, snapshot) {
return this.saveRecord(store, type, snapshot, {method: 'PUT'}, 'createRecord');
}
}

View File

@ -0,0 +1,14 @@
import ApplicationAdapter from './application';
import classic from 'ember-classic-decorator';
@classic
export default class Email extends ApplicationAdapter {
retry(model) {
let url = `${this.buildURL('email', model.get('id'))}retry/`;
return this.ajax(url, 'PUT', {data: {}}).then((data) => {
this.store.pushPayload(data);
return model;
});
}
}

View File

@ -0,0 +1,145 @@
import BaseAdapter from 'ghost-admin/adapters/base';
import classic from 'ember-classic-decorator';
import {get} from '@ember/object';
import {isNone} from '@ember/utils';
import {underscore} from '@ember/string';
// EmbeddedRelationAdapter will augment the query object in calls made to
// DS.Store#findRecord, findAll, query, and queryRecord with the correct "includes"
// (?include=relatedType) by introspecting on the provided subclass of the DS.Model.
// In cases where there is no query object (DS.Model#save, or simple finds) the URL
// that is built will be augmented with ?include=... where appropriate.
//
// Example:
// If a model has an embedded hasMany relation, the related type will be included:
// roles: DS.hasMany('role', { embedded: 'always' }) => ?include=roles
@classic
export default class EmbeddedRelationAdapter extends BaseAdapter {
find(store, type, id, snapshot) {
return this.ajax(this.buildIncludeURL(store, type.modelName, id, snapshot, 'find'), 'GET');
}
findRecord(store, type, id, snapshot) {
return this.ajax(this.buildIncludeURL(store, type.modelName, id, snapshot, 'findRecord'), 'GET');
}
findAll(store, type, sinceToken) {
let query, url;
if (sinceToken) {
query = {since: sinceToken};
}
url = this.buildIncludeURL(store, type.modelName, null, null, 'findAll');
return this.ajax(url, 'GET', {data: query});
}
query(store, type, query) {
return super.query(store, type, this.buildQuery(store, type.modelName, query));
}
queryRecord(store, type, query) {
return super.queryRecord(store, type, this.buildQuery(store, type.modelName, query));
}
createRecord(store, type, snapshot) {
return this.saveRecord(store, type, snapshot, {method: 'POST'}, 'createRecord');
}
updateRecord(store, type, snapshot) {
let options = {
method: 'PUT',
id: get(snapshot, 'id')
};
return this.saveRecord(store, type, snapshot, options, 'updateRecord');
}
saveRecord(store, type, snapshot, options, requestType) {
let _options = options || {};
let url = this.buildIncludeURL(store, type.modelName, _options.id, snapshot, requestType);
let payload = this.preparePayload(store, type, snapshot);
return this.ajax(url, _options.method, payload);
}
preparePayload(store, type, snapshot) {
let serializer = store.serializerFor(type.modelName);
let payload = {};
serializer.serializeIntoHash(payload, type, snapshot);
return {data: payload};
}
buildIncludeURL(store, modelName, id, snapshot, requestType, query) {
let includes = this.getEmbeddedRelations(store, modelName);
let url = this.buildURL(modelName, id, snapshot, requestType, query);
let parsedUrl = new URL(url);
if (includes.length) {
parsedUrl.searchParams.append('include', includes.map(underscore).join(','));
}
return parsedUrl.toString();
}
buildQuery(store, modelName, options) {
let deDupe = {};
let toInclude = this.getEmbeddedRelations(store, modelName);
let query = options || {};
if (toInclude.length) {
// If this is a find by id, build a query object and attach the includes
if (typeof options === 'string' || typeof options === 'number') {
query = {};
query.id = options;
query.include = toInclude.map(underscore).join(',');
} else if (typeof options === 'object' || isNone(options)) {
// If this is a find all (no existing query object) build one and attach
// the includes.
// If this is a find with an existing query object then merge the includes
// into the existing object. Existing properties and includes are preserved.
query = query || {};
toInclude = toInclude.concat(query.include ? query.include.split(',') : []);
toInclude.forEach((include) => {
deDupe[include] = true;
});
query.include = Object.keys(deDupe).map(underscore).join(',');
}
}
return query;
}
getEmbeddedRelations(store, modelName) {
let model = store.modelFor(modelName);
let ret = [];
let embedded = [];
// Iterate through the model's relationships and build a list
// of those that need to be pulled in via "include" from the API
model.eachRelationship((name, meta) => {
if (
meta.kind === 'hasMany'
&& Object.prototype.hasOwnProperty.call(meta.options, 'embedded')
&& meta.options.embedded === 'always'
) {
ret.push(name);
embedded.push([name, meta.type]);
}
});
embedded.forEach(([relName, embeddedModelName]) => {
this.getEmbeddedRelations(store, embeddedModelName).forEach((name) => {
ret.push(`${relName}.${name}`);
});
});
return ret;
}
}

View File

@ -0,0 +1,12 @@
import ApplicationAdapter from 'ghost-admin/adapters/application';
import SlugUrl from 'ghost-admin/utils/slug-url';
import classic from 'ember-classic-decorator';
@classic
export default class Label extends ApplicationAdapter {
buildURL(_modelName, _id, _snapshot, _requestType, query) {
let url = super.buildURL(...arguments);
return SlugUrl(url, query);
}
}

View File

@ -0,0 +1,27 @@
import ApplicationAdapter from 'ghost-admin/adapters/application';
import classic from 'ember-classic-decorator';
@classic
export default class Member extends ApplicationAdapter {
queryRecord(store, type, query) {
if (query && query.id) {
let {id} = query;
delete query.id;
let url = this.buildURL(type.modelName, id, query, 'findRecord');
return this.ajax(url, 'GET', {data: query});
}
return super.queryRecord(...arguments);
}
urlForDeleteRecord(id, modelName, snapshot) {
let url = super.urlForDeleteRecord(...arguments);
let parsedUrl = new URL(url);
if (snapshot && snapshot.adapterOptions && snapshot.adapterOptions.cancel) {
parsedUrl.searchParams.set('cancel', 'true');
}
return parsedUrl.toString();
}
}

View File

@ -0,0 +1,18 @@
import ApplicationAdapter from 'ghost-admin/adapters/application';
export default class Newsletter extends ApplicationAdapter {
buildIncludeURL(store, modelName, id, snapshot, requestType, query) {
const url = this.buildURL(modelName, id, snapshot, requestType, query);
const parsedUrl = new URL(url);
if (snapshot?.adapterOptions?.optInExisting) {
parsedUrl.searchParams.append('opt_in_existing', 'true');
}
if (snapshot?.adapterOptions?.include) {
parsedUrl.searchParams.append('include', snapshot?.adapterOptions?.include);
}
return parsedUrl.toString();
}
}

View File

@ -0,0 +1,23 @@
import ApplicationAdapter from 'ghost-admin/adapters/application';
import classic from 'ember-classic-decorator';
@classic
export default class Offer extends ApplicationAdapter {
queryRecord(store, type, query) {
if (query && query.id) {
let {id} = query;
delete query.id;
let url = this.buildURL(type.modelName, id, query, 'findRecord');
return this.ajax(url, 'GET', {data: query});
}
return super.queryRecord(...arguments);
}
urlForDeleteRecord() {
let url = super.urlForDeleteRecord(...arguments);
let parsedUrl = new URL(url);
return parsedUrl.toString();
}
}

View File

@ -0,0 +1,14 @@
import ApplicationAdapter from 'ghost-admin/adapters/application';
import classic from 'ember-classic-decorator';
@classic
export default class Page extends ApplicationAdapter {
// posts and pages now include everything by default
buildIncludeURL(store, modelName, id, snapshot, requestType, query) {
return this.buildURL(modelName, id, snapshot, requestType, query);
}
buildQuery(store, modelName, options) {
return options;
}
}

View File

@ -0,0 +1,30 @@
import ApplicationAdapter from 'ghost-admin/adapters/application';
export default class Post extends ApplicationAdapter {
// posts and pages now include everything by default
buildIncludeURL(store, modelName, id, snapshot, requestType, query) {
const url = this.buildURL(modelName, id, snapshot, requestType, query);
const parsedUrl = new URL(url);
if (snapshot?.adapterOptions?.newsletter) {
const newsletter = snapshot.adapterOptions.newsletter;
parsedUrl.searchParams.append('newsletter', newsletter);
let emailSegment = snapshot?.adapterOptions?.emailSegment;
if (emailSegment) {
if (emailSegment === 'status:free,status:-free') {
emailSegment = 'all';
}
parsedUrl.searchParams.append('email_segment', emailSegment);
}
}
return parsedUrl.toString();
}
buildQuery(store, modelName, options) {
return options;
}
}

View File

@ -0,0 +1,21 @@
import ApplicationAdapter from 'ghost-admin/adapters/application';
import classic from 'ember-classic-decorator';
@classic
export default class Setting extends ApplicationAdapter {
updateRecord(store, type, record) {
let data = {};
let serializer = store.serializerFor(type.modelName);
// remove the fake id that we added onto the model.
delete record.id;
// use the SettingSerializer to transform the model back into
// an array of settings objects like the API expects
serializer.serializeIntoHash(data, type, record);
// use the ApplicationAdapter's buildURL method but do not
// pass in an id.
return this.ajax(this.buildURL(type.modelName), 'PUT', {data});
}
}

View File

@ -0,0 +1,12 @@
import ApplicationAdapter from 'ghost-admin/adapters/application';
import SlugUrl from 'ghost-admin/utils/slug-url';
import classic from 'ember-classic-decorator';
@classic
export default class Tag extends ApplicationAdapter {
buildURL(_modelName, _id, _snapshot, _requestType, query) {
let url = super.buildURL(...arguments);
return SlugUrl(url, query);
}
}

View File

@ -0,0 +1,14 @@
import ApplicationAdapter from './application';
import classic from 'ember-classic-decorator';
@classic
export default class Theme extends ApplicationAdapter {
activate(model) {
let url = `${this.buildURL('theme', model.get('id'))}activate/`;
return this.ajax(url, 'PUT', {data: {}}).then((data) => {
this.store.pushPayload(data);
return model;
});
}
}

View File

@ -0,0 +1,23 @@
import ApplicationAdapter from 'ghost-admin/adapters/application';
import classic from 'ember-classic-decorator';
@classic
export default class Tier extends ApplicationAdapter {
queryRecord(store, type, query) {
if (query && query.id) {
let {id} = query;
delete query.id;
let url = this.buildURL(type.modelName, id, query, 'findRecord');
return this.ajax(url, 'GET', {data: query});
}
return super.queryRecord(...arguments);
}
urlForDeleteRecord() {
let url = super.urlForDeleteRecord(...arguments);
let parsedUrl = new URL(url);
return parsedUrl.toString();
}
}

View File

@ -0,0 +1,22 @@
import ApplicationAdapter from 'ghost-admin/adapters/application';
import SlugUrl from 'ghost-admin/utils/slug-url';
import classic from 'ember-classic-decorator';
@classic
export default class User extends ApplicationAdapter {
buildURL(_modelName, _id, _snapshot, _requestType, query) {
let url = super.buildURL(...arguments);
return SlugUrl(url, query);
}
queryRecord(store, type, query) {
if (!query || query.id !== 'me') {
return super.queryRecord(...arguments);
}
let url = this.buildURL(type.modelName, 'me', null, 'findRecord');
return this.ajax(url, 'GET', {data: {include: 'roles'}});
}
}

48
ghost/admin/app/app.js Executable file
View File

@ -0,0 +1,48 @@
import 'ghost-admin/utils/link-component';
import 'ghost-admin/utils/route';
import Application from '@ember/application';
import Resolver from 'ember-resolver';
import config from 'ghost-admin/config/environment';
import loadInitializers from 'ember-load-initializers';
import moment from 'moment';
import {registerWarnHandler} from '@ember/debug';
moment.updateLocale('en', {
relativeTime: {
m: '1 minute'
}
});
const App = Application.extend({
Resolver,
modulePrefix: config.modulePrefix,
podModulePrefix: config.podModulePrefix,
// eslint-disable-next-line
customEvents: {
touchstart: null,
touchmove: null,
touchend: null,
touchcancel: null
}
});
// TODO: remove once the validations refactor is complete
// eslint-disable-next-line
registerWarnHandler((message, options, next) => {
let skip = [
'ds.errors.add',
'ds.errors.remove',
'ds.errors.clear'
];
if (skip.includes(options.id)) {
return;
}
next(message, options);
});
loadInitializers(App, config.modulePrefix);
export default App;

View File

@ -0,0 +1,41 @@
import Authenticator from 'ember-simple-auth/authenticators/base';
import RSVP from 'rsvp';
import {computed} from '@ember/object';
import {inject as service} from '@ember/service';
export default Authenticator.extend({
ajax: service(),
ghostPaths: service(),
sessionEndpoint: computed('ghostPaths.apiRoot', function () {
return `${this.ghostPaths.apiRoot}/session`;
}),
restore: function () {
return RSVP.resolve();
},
authenticate(identification, password) {
const data = {username: identification, password};
const options = {
data,
contentType: 'application/json;charset=utf-8',
// ember-ajax will try and parse the response as JSON if not explicitly set
dataType: 'text'
};
return this.ajax.post(this.sessionEndpoint, options);
},
invalidate() {
// if we're invalidating because of a 401 we can end up in an infinite
// loop if we then try to perform a DELETE /session/ request
// TODO: find a more elegant way to handle this
if (this.ajax.skipSessionDeletion) {
this.ajax.skipSessionDeletion = false;
return RSVP.resolve();
}
return this.ajax.del(this.sessionEndpoint);
}
});

View File

@ -0,0 +1,3 @@
{{#unless this.isResizing}}
{{yield}}
{{/unless}}

View File

@ -0,0 +1,53 @@
import Component from '@ember/component';
import classic from 'ember-classic-decorator';
import {assert} from '@ember/debug';
import {debounce, run} from '@ember/runloop';
@classic
export default class AspectRatioBox extends Component {
ratio = '1/1';
base = 'height';
isResizing = true;
_ratio = 1;
init() {
super.init(...arguments);
this._onResizeHandler = () => {
debounce(this, this._resize, 200);
};
}
didReceiveAttrs() {
super.didReceiveAttrs(...arguments);
assert(
'{{aspect-ratio-box}} requires a `ratio` property in the format `"16/9"`',
this.ratio.match(/\d+\/\d+/)
);
this._ratio = this.ratio.split('/').reduce((prev, curr) => prev / curr);
}
didInsertElement() {
super.didInsertElement(...arguments);
this._resize();
window.addEventListener('resize', this._onResizeHandler);
}
willDestroyElement() {
super.willDestroyElement(...arguments);
window.removeEventListener('resize', this._onResizeHandler);
}
_resize() {
this.set('isResizing', true);
run.schedule('afterRender', this, function () {
if (this.base === 'height') {
this.element.style.width = `${this.element.clientHeight * this._ratio}px`;
} else {
this.element.style.height = `${this.element.clientWidth * this._ratio}px`;
}
this.set('isResizing', false);
});
}
}

View File

@ -0,0 +1,15 @@
<div class="gh-stack-item {{if (eq @index 0) "gh-setting-first" "gh-setting"}}">
<div class="flex-grow-1">
<div class="flex justify-between items-center relative">
<span class="gh-setting-title" for={{this.checkboxId}}>
{{humanize-setting-key @setting.key}}
</span>
<div class="for-switch x-small">
<label for={{this.checkboxId}} class="switch">
<input type="checkbox" class="gh-input" id={{this.checkboxId}} checked={{@setting.value}} {{on "input" this.toggleValue}}>
<span class="input-toggle-component mt1"></span>
</label>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,16 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';
import {camelize} from '@ember/string';
import {guidFor} from '@ember/object/internals';
export default class CustomThemeSettingsBooleanComponent extends Component {
checkboxId = `checkbox-${guidFor(this)}`;
checkboxName = camelize(this.args.setting.key);
@action
toggleValue(changeEvent) {
const value = changeEvent.target.checked;
this.args.setting.set('value', value);
this.args.onChange?.();
}
}

View File

@ -0,0 +1,43 @@
<div class="gh-stack-item {{if (eq @index 0) "gh-setting-first" "gh-setting"}}">
<div class="flex flex-grow-1 justify-between">
<div class="flex justify-between items-center relative">
<span class="gh-setting-title" for={{this.inputId}}>
{{humanize-setting-key @setting.key}}
</span>
</div>
<div>
<div class="input-color">
<input
type="text"
id={{this.inputId}}
name={{this.inputName}}
autocorrect="off"
maxlength="6"
value={{this.colorWithoutHash}}
class="gh-input"
{{on "input" (perform this.debounceValueUpdate)}}
{{on "blur" this.updateValue}}
{{on-key "Enter" this.blurElement}}
data-test-input="accentColor"
/>
<div class="color-picker-horizontal-divider"></div>
<div
class="color-box-container"
style={{this.this.colorPickerBgStyle}}
>
<input
type="color"
name={{this.inputName}}
class="color-picker"
value={{@setting.value}}
{{on "input" (perform this.debounceValueUpdate)}}
>
</div>
</div>
{{#if this.isInvalid}}
<div class="w-100 red">Please enter a color in hex format</div>
{{/if}}
</div>
</div>
</div>

View File

@ -0,0 +1,65 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';
import {camelize} from '@ember/string';
import {guidFor} from '@ember/object/internals';
import {htmlSafe} from '@ember/template';
import {task, timeout} from 'ember-concurrency';
import {tracked} from '@glimmer/tracking';
export default class CustomThemeSettingsColorComponent extends Component {
inputId = `input-${guidFor(this)}`;
inputName = camelize(this.args.setting.key);
@tracked isInvalid = false;
get colorWithoutHash() {
const color = this.args.setting.value;
if (color && color[0] === '#') {
return color.slice(1);
}
return color;
}
get colorPickerBgStyle() {
return htmlSafe(`background-color: ${this.args.setting.value || '#ffffff'}`);
}
@action
updateValue(event) {
const oldColor = this.args.setting.value;
let newColor = event.target.value;
if (!newColor) {
this.isInvalid = true;
return;
}
if (newColor[0] !== '#') {
newColor = `#${newColor}`;
}
if (newColor.match(/#[0-9A-Fa-f]{6}$/)) {
this.isInvalid = false;
if (newColor === oldColor) {
return;
}
this.args.setting.set('value', newColor);
this.args.onChange?.();
} else {
this.isInvalid = true;
}
}
@task({restartable: true})
*debounceValueUpdate(event) {
yield timeout(500);
this.updateValue(event);
}
@action
blurElement(event) {
event.target.blur();
}
}

View File

@ -0,0 +1,35 @@
<div class="gh-stack-item {{if (eq @index 0) "gh-setting-first" "gh-setting"}}">
<GhUploader
@extensions={{this.imageExtensions}}
@onComplete={{this.imageUploaded}}
as |uploader|
>
<div class="{{if @setting.value "" "flex flex-grow-1 items-center justify-between"}}">
<div class="gh-setting-content">
<div class="gh-setting-title {{if @setting.value "gh-theme-setting-title"}}">{{humanize-setting-key @setting.key}}</div>
{{#each uploader.errors as |error|}}
<div class="gh-setting-error" data-test-error="icon">{{or error.context error.message}}</div>
{{/each}}
</div>
<div class="gh-setting-action gh-uploadbutton-container flex flex-column items-stretch">
{{#if uploader.isUploading}}
{{uploader.progressBar}}
{{else if @setting.value}}
<div class="gh-branding-image-container largeimg justify-start">
<img class="blog-cover" src={{@setting.value}} {{on "click" uploader.triggerFileDialog}}>
<button type="button" class="gh-setting-action-largeimg-delete" {{on "click" (fn this.updateValue null)}} data-test-delete-image="icon">
{{svg-jar "trash" class="w4 h4 fill-white"}}
</button>
</div>
{{else}}
<button type="button" class="gh-btn gh-btn-white self-start" {{on "click" uploader.triggerFileDialog}} data-test-image-upload-btn="icon">
<span>Upload</span>
</button>
{{/if}}
<div style="display:none">
<GhFileInput @multiple={{false}} @action={{uploader.setFiles}} @accept={{this.imageMimeTypes}} @onInsert={{uploader.registerFileInput}} data-test-file-input="icon" />
</div>
</div>
</div>
</GhUploader>
</div>

View File

@ -0,0 +1,29 @@
import Component from '@glimmer/component';
import {
IMAGE_EXTENSIONS,
IMAGE_MIME_TYPES
} from 'ghost-admin/components/gh-image-uploader';
import {action} from '@ember/object';
import {camelize} from '@ember/string';
import {guidFor} from '@ember/object/internals';
export default class CustomThemeSettingsImageComponent extends Component {
inputId = `input-${guidFor(this)}`;
inputName = camelize(this.args.setting.key);
imageExtensions = IMAGE_EXTENSIONS;
imageMimeTypes = IMAGE_MIME_TYPES;
@action
imageUploaded(images) {
if (images[0]) {
this.updateValue(images[0].url);
}
}
@action
updateValue(value) {
this.args.setting.set('value', value);
this.args.onChange?.();
}
}

View File

@ -0,0 +1,19 @@
<div class="gh-stack-item {{if (eq @index 0) "gh-setting-first" "gh-setting"}}">
<div class="flex-grow-1">
<label class="gh-setting-title gh-theme-setting-title" for={{this.selectId}}>
{{humanize-setting-key @setting.key}}
</label>
<span class="gh-select">
<select class="ember-select" name={{this.selectName}} id={{this.selectId}} {{on "change" this.setSelection}}>
{{#each @setting.options as |settingOption|}}
<option value={{settingOption}} selected={{eq settingOption @setting.value}}>
{{settingOption}}
{{#if (eq settingOption @setting.default)}}(default){{/if}}
</option>
{{/each}}
</select>
{{svg-jar "arrow-down-small"}}
</span>
</div>
</div>

View File

@ -0,0 +1,16 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';
import {camelize} from '@ember/string';
import {guidFor} from '@ember/object/internals';
export default class CustomThemeSettingsSelectComponent extends Component {
selectId = `select-${guidFor(this)}`;
selectName = camelize(this.args.setting.key);
@action
setSelection(changeEvent) {
const value = changeEvent.target.value;
this.args.setting.set('value', value);
this.args.onChange?.();
}
}

View File

@ -0,0 +1,17 @@
<div class="gh-stack-item {{if (eq @index 0) "gh-setting-first" "gh-setting"}}">
<div class="flex-grow-1">
<label class="gh-setting-title gh-theme-setting-title" for={{this.inputId}}>
{{humanize-setting-key @setting.key}}
</label>
<input
type="text"
class="gh-input"
value={{@setting.value}}
id={{this.inputId}}
name={{this.inputName}}
{{on "input" this.updateValue}}
{{on "blur" this.triggerOnChange}}
/>
</div>
</div>

View File

@ -0,0 +1,19 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';
import {camelize} from '@ember/string';
import {guidFor} from '@ember/object/internals';
export default class CustomThemeSettingsTextComponent extends Component {
inputId = `input-${guidFor(this)}`;
inputName = camelize(this.args.setting.key);
@action
updateValue(event) {
this.args.setting.set('value', event.target.value);
}
@action
triggerOnChange() {
this.args.onChange?.();
}
}

View File

@ -0,0 +1,74 @@
<section class="gh-dashboard-section gh-dashboard-anchor" {{did-insert this.loadCharts}}>
<article class="gh-dashboard-box">
{{#if this.hasPaidTiers}}
<div class="gh-dashboard-select-title">
<PowerSelect
@selected={{this.selectedDisplayOption}}
@options={{this.displayOptions}}
@searchEnabled={{false}}
@onChange={{this.onDisplayChange}}
@triggerComponent="gh-power-select/trigger"
@triggerClass="gh-contentfilter-menu-trigger"
@dropdownClass="gh-contentfilter-menu-dropdown is-narrow"
@matchTriggerWidth={{false}}
@horizontalPosition="left"
as |option|
>
{{#if option.name}}{{option.name}}{{else}}<span class="red">Unknown option</span>{{/if}}
</PowerSelect>
</div>
{{else}}
<Dashboard::Parts::Metric
@label="Total members"
@value={{format-number this.totalMembers}}
@trends={{this.hasTrends}}
@percentage={{this.totalMembersTrend}}
@large={{true}} />
{{/if}}
<div class="gh-dashboard-hero {{unless this.hasPaidTiers 'is-solo'}}">
<div class="gh-dashboard-chart gh-dashboard-totals">
<div class="gh-dashboard-chart-container">
<div class="gh-dashboard-chart-box">
{{#if this.loading}}
<div class="gh-dashboard-chart-loading">
<div class="gh-loading-spinner"></div>
</div>
{{else}}
<div class="gh-dashboard-fader">
<EmberChart
@type={{this.chartType}}
@data={{this.chartData}}
@options={{this.chartOptions}}
@height={{200}} />
</div>
{{/if}}
</div>
<div id="gh-dashboard-anchor-tooltip" class="gh-dashboard-tooltip">
<div class="gh-dashboard-tooltip-label">
-
</div>
<div class="gh-dashboard-tooltip-value">
<span class="indicator line"></span>
<span class="value"></span>
<span class="metric">{{this.selectedDisplayOption.name}}</span>
</div>
</div>
</div>
<div class="gh-dashboard-chart-ticks">
<span id="gh-dashboard-anchor-date-start">-</span>
<span id="gh-dashboard-anchor-date-end">-</span>
</div>
</div>
{{#if this.hasPaidTiers}}
<article class="gh-dashboard-minicharts">
<Dashboard::Charts::PaidMrr />
<Dashboard::Charts::PaidBreakdown />
<Dashboard::Charts::PaidMix />
</article>
{{/if}}
</div>
</article>
</section>

View File

@ -0,0 +1,741 @@
/* global Chart */
import Component from '@glimmer/component';
import moment from 'moment';
import {action} from '@ember/object';
import {getSymbol} from 'ghost-admin/utils/currency';
import {inject as service} from '@ember/service';
import {tracked} from '@glimmer/tracking';
const DATE_FORMAT = 'D MMM, YYYY';
const DISPLAY_OPTIONS = [{
name: 'Total members',
value: 'total'
}, {
name: 'Paid members',
value: 'paid'
}, {
name: 'Free members',
value: 'free'
}];
// custom ChartJS draw function
Chart.defaults.hoverLine = Chart.defaults.line;
Chart.controllers.hoverLine = Chart.controllers.line.extend({
draw: function (ease) {
Chart.controllers.line.prototype.draw.call(this, ease);
if (this.chart.tooltip._active && this.chart.tooltip._active.length) {
let activePoint = this.chart.tooltip._active[0],
ctx = this.chart.ctx,
x = activePoint.tooltipPosition().x,
topY = this.chart.legend.bottom,
bottomY = this.chart.chartArea.bottom;
// draw line
ctx.save();
ctx.beginPath();
ctx.moveTo(x, topY);
ctx.lineTo(x, bottomY);
ctx.setLineDash([3, 4]);
ctx.lineWidth = 1;
ctx.strokeStyle = '#7C8B9A';
ctx.stroke();
ctx.restore();
}
}
});
export default class Anchor extends Component {
@service dashboardStats;
@service feature;
@tracked chartDisplay = 'total';
@tracked resizing = false;
@tracked resizeTimer = null;
displayOptions = DISPLAY_OPTIONS;
willDestroy(...args) {
super.willDestroy(...args);
window.removeEventListener('resize', this.resizer, false);
}
// this helps with ChartJS resizing stretching bug when resizing
resizer = () => {
this.resizing = true;
clearTimeout(this.resizeTimer); // this uses a trick to trigger only when resize is done
this.resizeTimer = setTimeout(() => {
this.resizing = false;
}, 500);
};
@action
loadCharts() {
this.dashboardStats.loadMemberCountStats();
window.addEventListener('resize', this.resizer, false);
if (this.hasPaidTiers) {
this.dashboardStats.loadMrrStats();
}
}
@action
onDisplayChange(selected) {
this.chartDisplay = selected.value;
}
get selectedDisplayOption() {
return this.displayOptions.find(d => d.value === this.chartDisplay) ?? this.displayOptions[0];
}
get loading() {
return this.dashboardStats.memberCountStats === null || this.resizing;
}
get totalMembers() {
return this.dashboardStats.memberCounts?.total ?? 0;
}
get isTotalMembersZero() {
return this.dashboardStats.memberCounts && this.totalMembers === 0;
}
get paidMembers() {
return this.dashboardStats.memberCounts?.paid ?? 0;
}
get freeMembers() {
return this.dashboardStats.memberCounts?.free ?? 0;
}
get hasTrends() {
return this.dashboardStats.memberCounts !== null
&& this.dashboardStats.memberCountsTrend !== null;
}
get totalMembersTrend() {
return this.calculatePercentage(this.dashboardStats.memberCountsTrend.total, this.dashboardStats.memberCounts.total);
}
get paidMembersTrend() {
return this.calculatePercentage(this.dashboardStats.memberCountsTrend.paid, this.dashboardStats.memberCounts.paid);
}
get freeMembersTrend() {
return this.calculatePercentage(this.dashboardStats.memberCountsTrend.free, this.dashboardStats.memberCounts.free);
}
get hasPaidTiers() {
return this.dashboardStats.siteStatus?.hasPaidTiers;
}
get chartType() {
return 'hoverLine'; // uses custom ChartJS draw function
}
get chartTitle() {
// paid
if (this.chartDisplay === 'paid') {
return 'Paid members';
// free
} else if (this.chartDisplay === 'free') {
return 'Free members';
}
// total
return 'Total members';
}
get chartData() {
let stats;
let labels;
let data;
if (this.chartDisplay === 'paid') {
// paid
stats = this.dashboardStats.filledMemberCountStats;
labels = stats.map(stat => stat.date);
data = stats.map(stat => stat.paid + stat.comped);
} else if (this.chartDisplay === 'free') {
// free
stats = this.dashboardStats.filledMemberCountStats;
labels = stats.map(stat => stat.date);
data = stats.map(stat => stat.free);
} else {
// total
stats = this.dashboardStats.filledMemberCountStats;
labels = stats.map(stat => stat.date);
data = stats.map(stat => stat.paid + stat.free + stat.comped);
}
// with no members yet, let's show empty state with dummy data
if (this.isTotalMembersZero) {
stats = this.emptyData.stats;
labels = this.emptyData.labels;
data = this.emptyData.data;
}
// gradient for line
const canvasLine = document.createElement('canvas');
const ctxLine = canvasLine.getContext('2d');
const gradientLine = ctxLine.createLinearGradient(0, 0, 1000, 0);
gradientLine.addColorStop(0, 'rgba(250, 45, 142, 1');
gradientLine.addColorStop(1, 'rgba(143, 66, 255, 1');
// gradient for fill
const canvasFill = document.createElement('canvas');
const ctxFill = canvasFill.getContext('2d');
const gradientFill = ctxFill.createLinearGradient(0, 0, 1000, 0);
gradientFill.addColorStop(0, 'rgba(250, 45, 142, 0.2');
gradientFill.addColorStop(1, 'rgba(143, 66, 255, 0.1');
return {
labels: labels,
datasets: [{
data: data,
tension: 1,
cubicInterpolationMode: 'monotone',
fill: true,
fillColor: gradientFill,
backgroundColor: gradientFill,
pointRadius: 0,
pointHitRadius: 10,
pointBorderColor: '#8E42FF',
pointBackgroundColor: '#8E42FF',
pointHoverBackgroundColor: '#8E42FF',
pointHoverBorderColor: '#8E42FF',
pointHoverRadius: 0,
borderColor: gradientLine,
borderJoinStyle: 'miter'
}]
};
}
get mrrCurrencySymbol() {
if (this.dashboardStats.mrrStats === null) {
return '';
}
const firstCurrency = this.dashboardStats.mrrStats[0] ? this.dashboardStats.mrrStats[0].currency : 'usd';
return getSymbol(firstCurrency);
}
get chartOptions() {
let activeDays = this.dashboardStats.chartDays;
let barColor = this.feature.nightShift ? 'rgba(200, 204, 217, 0.25)' : 'rgba(200, 204, 217, 0.65)';
return {
maintainAspectRatio: false,
responsiveAnimationDuration: 1,
animation: false,
title: {
display: false
},
legend: {
display: false
},
layout: {
padding: {
top: 2,
bottom: 2,
left: 1,
right: 1
}
},
hover: {
onHover: function (e) {
e.target.style.cursor = 'pointer';
}
},
tooltips: {
enabled: false,
intersect: false,
mode: 'index',
custom: function (tooltip) {
// get tooltip element
const tooltipEl = document.getElementById('gh-dashboard-anchor-tooltip');
const chartContainerEl = tooltipEl.parentElement;
const chartWidth = chartContainerEl.offsetWidth;
const tooltipWidth = tooltipEl.offsetWidth;
// only show tooltip when active
if (tooltip.opacity === 0) {
tooltipEl.style.opacity = 0;
return;
}
// update tooltip styles
tooltipEl.style.opacity = 1;
tooltipEl.style.position = 'absolute';
let offsetX = 0;
if (tooltip.x > chartWidth - tooltipWidth) {
offsetX = tooltipWidth - 10;
}
tooltipEl.style.left = tooltip.x - offsetX + 'px';
tooltipEl.style.top = tooltip.y + 'px';
},
callbacks: {
label: (tooltipItems, data) => {
const value = data.datasets[tooltipItems.datasetIndex].data[tooltipItems.index].toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
document.querySelector('#gh-dashboard-anchor-tooltip .gh-dashboard-tooltip-value .value').innerHTML = value;
},
title: (tooltipItems) => {
const value = moment(tooltipItems[0].xLabel).format(DATE_FORMAT);
document.querySelector('#gh-dashboard-anchor-tooltip .gh-dashboard-tooltip-label').innerHTML = value;
}
}
},
scales: {
yAxes: [{
display: true,
gridLines: {
drawTicks: false,
display: true,
drawBorder: false,
color: 'transparent',
zeroLineColor: barColor,
zeroLineWidth: 1
},
ticks: {
display: false
}
}],
xAxes: [{
display: true,
scaleLabel: {
align: 'start'
},
gridLines: {
color: barColor,
borderDash: [3,4],
display: true,
drawBorder: true,
drawTicks: false,
zeroLineWidth: 1,
zeroLineColor: barColor,
zeroLineBorderDash: [3,4]
},
ticks: {
display: false,
autoSkip: false,
callback: function (value, index, values) {
if (index === 0) {
document.getElementById('gh-dashboard-anchor-date-start').innerHTML = moment(value).format(DATE_FORMAT);
}
if (index === (values.length - 1)) {
document.getElementById('gh-dashboard-anchor-date-end').innerHTML = moment(value).format(DATE_FORMAT);
}
if (activeDays === (30 + 1)) {
if (!(index % 2)) {
return value;
}
} else if (activeDays === (90 + 1)) {
if (!(index % 3)) {
return value;
}
} else {
return value;
}
}
}
}]
}
};
}
get chartHeight() {
return 200;
}
get chartHeightSmall() {
return 180;
}
// used for empty state
get emptyData() {
return {
stats: [
{
date: '2022-04-07',
free: 2610,
tier1: 295,
tier2: 20,
paid: 315,
comped: 0,
paidSubscribed: 2,
paidCanceled: 1
},
{
date: '2022-04-08',
free: 2765,
tier1: 298,
tier2: 24,
paid: 322,
comped: 0,
paidSubscribed: 7,
paidCanceled: 0
},
{
date: '2022-04-09',
free: 3160,
tier1: 299,
tier2: 28,
paid: 327,
comped: 0,
paidSubscribed: 5,
paidCanceled: 0
},
{
date: '2022-04-10',
free: 3580,
tier1: 300,
tier2: 30,
paid: 330,
comped: 0,
paidSubscribed: 4,
paidCanceled: 1
},
{
date: '2022-04-11',
free: 3583,
tier1: 301,
tier2: 31,
paid: 332,
comped: 0,
paidSubscribed: 2,
paidCanceled: 0
},
{
date: '2022-04-12',
free: 3857,
tier1: 303,
tier2: 36,
paid: 339,
comped: 0,
paidSubscribed: 8,
paidCanceled: 1
},
{
date: '2022-04-13',
free: 4223,
tier1: 304,
tier2: 39,
paid: 343,
comped: 0,
paidSubscribed: 4,
paidCanceled: 0
},
{
date: '2022-04-14',
free: 4289,
tier1: 306,
tier2: 42,
paid: 348,
comped: 0,
paidSubscribed: 6,
paidCanceled: 1
},
{
date: '2022-04-15',
free: 4458,
tier1: 307,
tier2: 49,
paid: 356,
comped: 0,
paidSubscribed: 8,
paidCanceled: 0
},
{
date: '2022-04-16',
free: 4752,
tier1: 307,
tier2: 49,
paid: 356,
comped: 0,
paidSubscribed: 1,
paidCanceled: 1
},
{
date: '2022-04-17',
free: 4947,
tier1: 310,
tier2: 50,
paid: 360,
comped: 0,
paidSubscribed: 5,
paidCanceled: 1
},
{
date: '2022-04-18',
free: 5047,
tier1: 312,
tier2: 49,
paid: 361,
comped: 0,
paidSubscribed: 2,
paidCanceled: 1
},
{
date: '2022-04-19',
free: 5430,
tier1: 314,
tier2: 55,
paid: 369,
comped: 0,
paidSubscribed: 8,
paidCanceled: 0
},
{
date: '2022-04-20',
free: 5760,
tier1: 316,
tier2: 57,
paid: 373,
comped: 0,
paidSubscribed: 4,
paidCanceled: 0
},
{
date: '2022-04-21',
free: 6022,
tier1: 318,
tier2: 63,
paid: 381,
comped: 0,
paidSubscribed: 9,
paidCanceled: 1
},
{
date: '2022-04-22',
free: 6294,
tier1: 319,
tier2: 64,
paid: 383,
comped: 0,
paidSubscribed: 2,
paidCanceled: 0
},
{
date: '2022-04-23',
free: 6664,
tier1: 320,
tier2: 69,
paid: 389,
comped: 0,
paidSubscribed: 6,
paidCanceled: 0
},
{
date: '2022-04-24',
free: 6721,
tier1: 320,
tier2: 70,
paid: 390,
comped: 0,
paidSubscribed: 1,
paidCanceled: 0
},
{
date: '2022-04-25',
free: 6841,
tier1: 321,
tier2: 80,
paid: 401,
comped: 0,
paidSubscribed: 11,
paidCanceled: 0
},
{
date: '2022-04-26',
free: 6880,
tier1: 323,
tier2: 89,
paid: 412,
comped: 0,
paidSubscribed: 11,
paidCanceled: 0
},
{
date: '2022-04-27',
free: 7179,
tier1: 325,
tier2: 92,
paid: 417,
comped: 0,
paidSubscribed: 5,
paidCanceled: 0
},
{
date: '2022-04-28',
free: 7288,
tier1: 325,
tier2: 100,
paid: 425,
comped: 0,
paidSubscribed: 9,
paidCanceled: 1
},
{
date: '2022-04-29',
free: 7430,
tier1: 325,
tier2: 101,
paid: 426,
comped: 0,
paidSubscribed: 2,
paidCanceled: 1
},
{
date: '2022-04-30',
free: 7458,
tier1: 326,
tier2: 102,
paid: 428,
comped: 0,
paidSubscribed: 2,
paidCanceled: 0
},
{
date: '2022-05-01',
free: 7621,
tier1: 327,
tier2: 117,
paid: 444,
comped: 0,
paidSubscribed: 17,
paidCanceled: 1
},
{
date: '2022-05-02',
free: 7721,
tier1: 328,
tier2: 123,
paid: 451,
comped: 0,
paidSubscribed: 8,
paidCanceled: 1
},
{
date: '2022-05-03',
free: 7897,
tier1: 327,
tier2: 137,
paid: 464,
comped: 0,
paidSubscribed: 14,
paidCanceled: 1
},
{
date: '2022-05-04',
free: 7937,
tier1: 327,
tier2: 143,
paid: 470,
comped: 0,
paidSubscribed: 6,
paidCanceled: 0
},
{
date: '2022-05-05',
free: 7961,
tier1: 328,
tier2: 158,
paid: 486,
comped: 0,
paidSubscribed: 16,
paidCanceled: 0
},
{
date: '2022-05-06',
free: 8006,
tier1: 328,
tier2: 162,
paid: 490,
comped: 0,
paidSubscribed: 5,
paidCanceled: 1
}
],
labels: [
'2022-04-07',
'2022-04-08',
'2022-04-09',
'2022-04-10',
'2022-04-11',
'2022-04-12',
'2022-04-13',
'2022-04-14',
'2022-04-15',
'2022-04-16',
'2022-04-17',
'2022-04-18',
'2022-04-19',
'2022-04-20',
'2022-04-21',
'2022-04-22',
'2022-04-23',
'2022-04-24',
'2022-04-25',
'2022-04-26',
'2022-04-27',
'2022-04-28',
'2022-04-29',
'2022-04-30',
'2022-05-01',
'2022-05-02',
'2022-05-03',
'2022-05-04',
'2022-05-05',
'2022-05-06'
],
data: [
2925,
3087,
3487,
3910,
3915,
4196,
4566,
4637,
4814,
5108,
5307,
5408,
5799,
6133,
6403,
6677,
7053,
7111,
7242,
7292,
7596,
7713,
7856,
7886,
8065,
8172,
8361,
8407,
8447,
8496
]
};
}
calculatePercentage(from, to) {
if (from === 0) {
if (to > 0) {
return 100;
}
return 0;
}
return Math.round((to - from) / from * 100);
}
}

View File

@ -0,0 +1,49 @@
<section class="gh-dashboard-section gh-dashboard-engagement">
<article {{did-insert this.loadCharts}} class="gh-dashboard-box">
<Dashboard::Parts::Metric
@label="Engagement" />
<div class="gh-dashboard-columns">
<div class="gh-dashboard-column gh-dashboard-engagement-30days">
<Dashboard::Parts::Metric
@label="Engaged in the last 30 days"
@value={{this.data30Days}}
@secondary={{true}}
/>
</div>
<div class="gh-dashboard-column gh-dashboard-engagement-7days">
<Dashboard::Parts::Metric
@label="Engaged in the last 7 days"
@value={{this.data7Days}}
@secondary={{true}}
/>
</div>
<div class="gh-dashboard-column gh-dashboard-engagement-subscribers">
<Dashboard::Parts::Metric
@label="Newsletter subscribers"
@value={{this.dataSubscribers}}
@secondary={{true}}
/>
</div>
</div>
</article>
{{#if this.hasPaidTiers}}
<div class="gh-dashboard-select">
<PowerSelect
@selected={{this.selectedStatusOption}}
@options={{this.statusOptions}}
@searchEnabled={{false}}
@onChange={{this.onSwitchStatus}}
@triggerComponent="gh-power-select/trigger"
@triggerClass="gh-contentfilter-menu-trigger"
@dropdownClass="gh-contentfilter-menu-dropdown is-narrow"
@matchTriggerWidth={{false}}
@horizontalPosition="right"
as |option|
>
{{#if option.name}}{{option.name}}{{else}}<span class="red">Unknown option</span>{{/if}}
</PowerSelect>
</div>
{{/if}}
</section>

View File

@ -0,0 +1,118 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';
import {formatNumber} from '../../../helpers/format-number';
import {inject as service} from '@ember/service';
import {tracked} from '@glimmer/tracking';
const STATUS_OPTIONS = [{
name: 'All members',
value: 'total'
}, {
name: 'Paid members',
value: 'paid'
}, {
name: 'Free members',
value: 'free'
}];
export default class Engagement extends Component {
@service dashboardStats;
@action
loadCharts() {
this.dashboardStats.lastSeenFilterStatus = this.status;
this.dashboardStats.loadLastSeen();
this.dashboardStats.loadMemberCountStats();
this.dashboardStats.loadNewsletterSubscribers();
}
@tracked status = 'total';
statusOptions = STATUS_OPTIONS;
get selectedStatusOption() {
return this.statusOptions.find(option => option.value === this.status);
}
@action
onSwitchStatus(selected) {
this.status = selected.value;
this.dashboardStats.lastSeenFilterStatus = this.status;
this.dashboardStats.loadLastSeen();
}
get loading() {
return this.dashboardStats.memberCounts === null
|| !this.dashboardStats.memberCounts[this.status]
|| this.dashboardStats.membersLastSeen30d === null
|| this.dashboardStats.membersLastSeen7d === null;
}
get data30Days() {
// fake empty data
if (this.isTotalMembersZero) {
return '30%';
}
if (this.loading) {
return '- %';
}
const total = this.dashboardStats.memberCounts[this.status];
const part = this.dashboardStats.membersLastSeen30d;
if (total <= 0) {
return '- %';
}
const percentage = Math.round(part / total * 100);
return `${percentage}%`;
}
get data7Days() {
// fake empty data
if (this.isTotalMembersZero) {
return '60%';
}
if (this.loading) {
return '- %';
}
const total = this.dashboardStats.memberCounts[this.status];
const part = this.dashboardStats.membersLastSeen7d;
if (total <= 0) {
return '- %';
}
const percentage = Math.round(part / total * 100);
return `${percentage}%`;
}
get dataSubscribers() {
// fake empty data
if (this.isTotalMembersZero) {
return '123';
}
if (!this.dashboardStats.newsletterSubscribers) {
return '-';
}
return formatNumber(this.dashboardStats.newsletterSubscribers[this.status]);
}
get dataEmailsSent() {
return this.dashboardStats.emailsSent30d ?? 0;
}
get hasPaidTiers() {
return this.dashboardStats.siteStatus?.hasPaidTiers;
}
get totalMembers() {
return this.dashboardStats.memberCounts?.total ?? 0;
}
get isTotalMembersZero() {
return this.dashboardStats.memberCounts && this.totalMembers === 0;
}
}

View File

@ -0,0 +1,30 @@
<section class="gh-dashboard-section gh-dashboard-overview {{unless this.isTotalMembersMoreThanZero 'is-hidden'}}">
<article {{did-insert this.loadCharts}} class="gh-dashboard-box is-secondary">
<div class="gh-dashboard-columns">
<div class="gh-dashboard-column">
<Dashboard::Parts::Metric
@label={{gh-pluralize this.totalMembers "Total member" without-count=true}}
@value={{this.totalMembersFormatted}}
@trends={{this.hasTrends}}
@percentage={{this.totalMembersTrend}}
@large={{true}} />
</div>
<div class="gh-dashboard-column">
<Dashboard::Parts::Metric
@label={{gh-pluralize this.paidMembers "Paid member" without-count=true}}
@value={{this.paidMembersFormatted}}
@trends={{this.hasTrends}}
@percentage={{this.paidMembersTrend}}
@large={{true}} />
</div>
<div class="gh-dashboard-column">
<Dashboard::Parts::Metric
@label={{gh-pluralize this.freeMembers "Free member" without-count=true}}
@value={{this.freeMembersFormatted}}
@trends={{this.hasTrends}}
@percentage={{this.freeMembersTrend}}
@large={{true}} />
</div>
</div>
</article>
</section>

View File

@ -0,0 +1,88 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';
import {formatNumber} from '../../../helpers/format-number';
import {inject as service} from '@ember/service';
export default class Overview extends Component {
@service dashboardStats;
@action
loadCharts() {
this.dashboardStats.loadMemberCountStats();
}
get loading() {
return this.dashboardStats.memberCountStats === null;
}
get totalMembers() {
return this.dashboardStats.memberCounts?.total ?? 0;
}
get isTotalMembersMoreThanZero() {
return this.dashboardStats.memberCounts && this.totalMembers > 0;
}
get paidMembers() {
return this.dashboardStats.memberCounts?.paid ?? 0;
}
get freeMembers() {
return this.dashboardStats.memberCounts?.free ?? 0;
}
get totalMembersFormatted() {
if (this.dashboardStats.memberCounts === null) {
return '-';
}
return formatNumber(this.totalMembers);
}
get paidMembersFormatted() {
if (this.dashboardStats.memberCounts === null) {
return '-';
}
return formatNumber(this.paidMembers);
}
get freeMembersFormatted() {
if (this.dashboardStats.memberCounts === null) {
return '-';
}
return formatNumber(this.freeMembers);
}
get hasTrends() {
return this.dashboardStats.memberCounts !== null
&& this.dashboardStats.memberCountsTrend !== null
&& this.dashboardStats.currentMRR !== null
&& this.dashboardStats.currentMRRTrend !== null;
}
get totalMembersTrend() {
return this.calculatePercentage(this.dashboardStats.memberCountsTrend.total, this.dashboardStats.memberCounts.total);
}
get paidMembersTrend() {
return this.calculatePercentage(this.dashboardStats.memberCountsTrend.paid, this.dashboardStats.memberCounts.paid);
}
get freeMembersTrend() {
return this.calculatePercentage(this.dashboardStats.memberCountsTrend.free, this.dashboardStats.memberCounts.free);
}
get hasPaidTiers() {
return this.dashboardStats.siteStatus?.hasPaidTiers;
}
calculatePercentage(from, to) {
if (from === 0) {
if (to > 0) {
return 100;
}
return 0;
}
return Math.round((to - from) / from * 100);
}
}

View File

@ -0,0 +1,43 @@
<div class="gh-dashboard-minichart gh-dashboard-breakdown">
<div class="gh-dashboard-content">
<div class="gh-dashboard-data">
<Dashboard::Parts::Metric
@label={{this.chartTitle}} />
<div class="gh-dashboard-legend">
<div class="gh-dashboard-legend-item">New</div>
<div class="gh-dashboard-legend-item">Canceled</div>
</div>
</div>
<div class="gh-dashboard-chart" {{did-insert this.loadCharts}}>
{{#if this.loading}}
<div class="gh-dashboard-chart-loading">
<div class="gh-loading-spinner"></div>
</div>
{{else}}
<div class="gh-dashboard-chart-container">
<div class="gh-dashboard-chart-box">
<EmberChart
@type={{this.chartType}}
@data={{this.chartData}}
@options={{this.chartOptions}}
@height={{110}} />
</div>
<div id="gh-dashboard-breakdown-tooltip" class="gh-dashboard-tooltip">
<div class="gh-dashboard-tooltip-label">
-
</div>
<div class="gh-dashboard-tooltip-value">
<div class="gh-dashboard-tooltip-value-1"><span class="indicator solid"></span><span class="value"></span></div>
<div class="gh-dashboard-tooltip-value-1"><span class="metric">New</span></div>
<div class="gh-dashboard-tooltip-value-2"><span class="indicator solid"></span><span class="value"></span></div>
<div class="gh-dashboard-tooltip-value-2"><span class="metric">Canceled</span></div>
<div class="gh-dashboard-tooltip-value-3"></div>
</div>
</div>
</div>
{{/if}}
</div>
</div>
</div>

View File

@ -0,0 +1,344 @@
/* globals Chart */
import Component from '@glimmer/component';
import moment from 'moment';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
const DATE_FORMAT = 'D MMM, YYYY';
// Custom ChartJS rounded rectangle
Chart.elements.RoundedRectangle = Chart.elements.Rectangle.extend({
draw: function () {
var ctx = this._chart.ctx;
var vm = this._view;
var left, right, top, bottom, borderSkipped, radius;
// If radius is less than 0 or is large enough to cause drawing errors a max
// radius is imposed. If cornerRadius is not defined set it to 0.
var cornerRadius = this._chart.config.options.cornerRadius;
var fullCornerRadius = this._chart.config.options.fullCornerRadius;
var stackedRounded = this._chart.config.options.stackedRounded;
if (cornerRadius < 0) {
cornerRadius = 0;
}
if (typeof cornerRadius === 'undefined') {
cornerRadius = 0;
}
if (typeof fullCornerRadius === 'undefined') {
fullCornerRadius = true;
}
if (typeof stackedRounded === 'undefined') {
stackedRounded = false;
}
left = vm.x - vm.width / 2;
right = vm.x + vm.width / 2;
top = vm.y;
bottom = vm.base;
borderSkipped = vm.borderSkipped || 'bottom';
ctx.beginPath();
ctx.fillStyle = vm.backgroundColor;
ctx.strokeStyle = vm.borderColor;
// Corner points, from bottom-left to bottom-right clockwise
// | 1 2 |
// | 0 3 |
var corners = [
[left, bottom],
[left, top],
[right, top],
[right, bottom]
];
// Find first (starting) corner with fallback to 'bottom'
var borders = ['bottom', 'left', 'top', 'right'];
var startCorner = borders.indexOf(borderSkipped, 0);
if (startCorner === -1) {
startCorner = 0;
}
function cornerAt(index) {
return corners[(startCorner + index) % 4];
}
// Draw rectangle from 'startCorner'
var corner = cornerAt(0);
ctx.moveTo(corner[0], corner[1]);
var nextCornerId, width, height, x, y;
for (var i = 1; i < 4; i++) {
corner = cornerAt(i);
nextCornerId = i + 1;
if (nextCornerId === 4) {
nextCornerId = 0;
}
width = corners[2][0] - corners[1][0];
height = corners[0][1] - corners[1][1];
x = corners[1][0];
y = corners[1][1];
radius = cornerRadius;
// Fix radius being too large
if (radius > Math.abs(height) / 2) {
radius = Math.floor(Math.abs(height) / 2);
}
if (radius > Math.abs(width) / 2) {
radius = Math.floor(Math.abs(width) / 2);
}
var xTL, xTR, yTL, yTR, xBL, xBR, yBL, yBR;
if (height < 0) {
// Negative values in a standard bar chart
xTL = x;
xTR = x + width;
yTL = y + height;
yTR = y + height;
xBL = x;
xBR = x + width;
yBL = y;
yBR = y;
// Draw
ctx.moveTo(xBL + radius, yBL);
ctx.lineTo(xBR - radius, yBR);
// bottom right
ctx.quadraticCurveTo(xBR, yBR, xBR, yBR - radius);
ctx.lineTo(xTR, yTR + radius);
// top right
ctx.lineTo(xTR, yTR, xTR - radius, yTR);
ctx.lineTo(xTL + radius, yTL);
// top left
ctx.lineTo(xTL, yTL, xTL, yTL + radius);
ctx.lineTo(xBL, yBL - radius);
// bottom left
ctx.quadraticCurveTo(xBL, yBL, xBL + radius, yBL);
} else {
// Positive values in a standard bar chart
ctx.moveTo(x + radius, y);
ctx.lineTo(x + width - radius, y);
// top right
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
ctx.lineTo(x + width, y + height - radius);
// bottom right
ctx.lineTo(x + width, y + height, x + width - radius, y + height);
ctx.lineTo(x + radius, y + height);
ctx.lineTo(x, y + height, x, y + height - radius);
ctx.lineTo(x, y + radius);
// top left
ctx.quadraticCurveTo(x, y, x + radius, y);
}
}
ctx.fill();
}
});
Chart.defaults.hoverBar = Chart.defaults.bar;
Chart.controllers.hoverBar = Chart.controllers.bar.extend({
draw: function (ease) {
Chart.controllers.bar.prototype.draw.call(this, ease);
var ctx = this.chart.ctx;
if (this.chart.tooltip._active && this.chart.tooltip._active.length) {
let activePoint = this.chart.tooltip._active[0],
x = activePoint.tooltipPosition().x,
topY = this.chart.legend.bottom,
bottomY = this.chart.chartArea.bottom;
// draw line
ctx.save();
ctx.beginPath();
ctx.moveTo(x, topY);
ctx.lineTo(x, bottomY);
ctx.setLineDash([3, 4]);
ctx.lineWidth = 1;
ctx.strokeStyle = '#7C8B9A';
ctx.stroke();
ctx.restore();
}
},
dataElementType: Chart.elements.RoundedRectangle
});
export default class PaidBreakdown extends Component {
@service dashboardStats;
@service feature;
@action
loadCharts() {
this.dashboardStats.loadSubscriptionCountStats();
}
get loading() {
return this.dashboardStats.subscriptionCountStats === null;
}
get chartTitle() {
return 'Paid subscribers';
}
get chartType() {
return 'hoverBar';
}
get chartData() {
const stats = this.dashboardStats.filledSubscriptionCountStats;
const labels = stats.map(stat => stat.date);
const newData = stats.map(stat => stat.signups);
const canceledData = stats.map(stat => -stat.cancellations);
let barThickness = 5;
if (newData.length >= 30 + 1 && newData.length < 90) {
barThickness = 3.5;
} else if (newData.length >= 90) {
barThickness = 1.5;
}
return {
labels: labels,
datasets: [
{
data: newData,
backgroundColor: '#8E42FF',
cubicInterpolationMode: 'monotone',
barThickness: barThickness,
minBarLength: 3
}, {
data: canceledData,
backgroundColor: '#FB76B4',
cubicInterpolationMode: 'monotone',
barThickness: barThickness,
minBarLength: 3
}]
};
}
get chartOptions() {
const barColor = this.feature.nightShift ? 'rgba(200, 204, 217, 0.25)' : 'rgba(200, 204, 217, 0.65)';
return {
responsive: true,
cornerRadius: 50,
fullCornerRadius: false,
maintainAspectRatio: false,
title: {
display: false
},
legend: {
display: false
},
layout: {
padding: {
top: 4,
bottom: 0,
left: 0,
right: 0
}
},
hover: {
onHover: function (e) {
e.target.style.cursor = 'pointer';
}
},
animation: false,
responsiveAnimationDuration: 1,
tooltips: {
enabled: false,
intersect: false,
mode: 'index',
custom: function (tooltip) {
// get tooltip element
const tooltipEl = document.getElementById('gh-dashboard-breakdown-tooltip');
const chartContainerEl = tooltipEl.parentElement;
const chartWidth = chartContainerEl.offsetWidth;
const tooltipWidth = tooltipEl.offsetWidth;
// only show tooltip when active
if (tooltip.opacity === 0) {
tooltipEl.style.display = 'none';
tooltipEl.style.opacity = 0;
return;
}
let offsetX = 0;
if (tooltip.x > chartWidth - tooltipWidth) {
offsetX = tooltipWidth - 10;
}
// update tooltip styles
tooltipEl.style.display = 'block';
tooltipEl.style.opacity = 1;
tooltipEl.style.position = 'absolute';
tooltipEl.style.left = tooltip.x - offsetX + 'px';
tooltipEl.style.top = '70px';
},
callbacks: {
label: (tooltipItems, data) => {
// new data
let newValue = parseInt(data.datasets[0].data[tooltipItems.index].toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','));
document.querySelector('#gh-dashboard-breakdown-tooltip .gh-dashboard-tooltip-value-1 .value').innerHTML = `${newValue}`;
// canceld data
let canceledValue = Math.abs(parseInt(data.datasets[1].data[tooltipItems.index].toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')));
document.querySelector('#gh-dashboard-breakdown-tooltip .gh-dashboard-tooltip-value-2 .value').innerHTML = `${canceledValue}`;
},
title: (tooltipItems) => {
const value = moment(tooltipItems[0].xLabel).format(DATE_FORMAT);
document.querySelector('#gh-dashboard-breakdown-tooltip .gh-dashboard-tooltip-label').innerHTML = value;
}
}
},
scales: {
yAxes: [{
offset: false,
gridLines: {
drawTicks: false,
display: true,
drawBorder: false,
color: 'rgba(255, 255, 255, 0.1)',
lineWidth: 0,
zeroLineColor: barColor,
zeroLineWidth: 1
},
ticks: {
display: false,
fontColor: '#7C8B9A',
padding: 8,
precision: 0
}
}],
xAxes: [{
offset: true,
stacked: true,
gridLines: {
color: barColor,
borderDash: [4,4],
display: false,
drawBorder: false,
drawTicks: false,
zeroLineWidth: 1,
zeroLineColor: barColor,
zeroLineBorderDash: [4,4]
},
ticks: {
display: false
}
}]
}
};
}
}

View File

@ -0,0 +1,57 @@
<div class="gh-dashboard-minichart gh-dashboard-mix {{if this.isChartCadence 'is-cadence'}}">
<div class="gh-dashboard-content">
<div class="gh-dashboard-data">
<Dashboard::Parts::Metric
@label="Paid mix" />
{{#if this.isChartCadence}}
<div class="gh-dashboard-legend">
<div class="gh-dashboard-legend-item">Monthly</div>
<div class="gh-dashboard-legend-item">Annual</div>
</div>
{{/if}}
</div>
<div class="gh-dashboard-chart" {{did-insert this.loadCharts}}>
{{#if this.loading}}
<div class="gh-dashboard-chart-loading">
<div class="gh-loading-spinner"></div>
</div>
{{else}}
<div class="gh-dashboard-chart-container">
<div class="gh-dashboard-chart-box">
<EmberChart
@type={{this.chartType}}
@data={{this.chartData}}
@options={{this.chartOptions}}
@height={{110}} />
</div>
<div id="gh-dashboard-mix-tooltip" class="gh-dashboard-tooltip">
<div class="gh-dashboard-tooltip-value">
-
</div>
</div>
</div>
{{/if}}
</div>
</div>
{{#if this.hasMultipleTiers }}
<div class="gh-dashboard-select">
<PowerSelect
@selected={{this.selectedModeOption}}
@options={{this.modeOptions}}
@searchEnabled={{false}}
@onChange={{this.onSwitchMode}}
@triggerComponent="gh-power-select/trigger"
@triggerClass="gh-contentfilter-menu-trigger"
@dropdownClass="gh-contentfilter-menu-dropdown is-narrow"
@matchTriggerWidth={{false}}
@horizontalPosition="right"
as |option|
>
{{#if option.name}}{{option.name}}{{else}}<span class="red">Unknown option</span>{{/if}}
</PowerSelect>
</div>
{{/if}}
</div>

View File

@ -0,0 +1,524 @@
/* globals Chart */
import Component from '@glimmer/component';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
import {tracked} from '@glimmer/tracking';
const MODE_OPTIONS = [{
name: 'Cadence',
value: 'cadence'
}, {
name: 'Tiers',
value: 'tiers'
}];
// Custom ChartJS rounded rectangle
Chart.elements.Rectangle.prototype.draw = function () {
var ctx = this._chart.ctx;
var vm = this._view;
var left, right, top, bottom, borderSkipped, radius;
// If radius is less than 0 or is large enough to cause drawing errors a max
// radius is imposed. If cornerRadius is not defined set it to 0.
var cornerRadius = this._chart.config.options.cornerRadius;
var fullCornerRadius = this._chart.config.options.fullCornerRadius;
var stackedRounded = this._chart.config.options.stackedRounded;
var typeOfChart = this._chart.config.type;
if (cornerRadius < 0) {
cornerRadius = 0;
}
if (typeof cornerRadius === 'undefined') {
cornerRadius = 0;
}
if (typeof fullCornerRadius === 'undefined') {
fullCornerRadius = true;
}
if (typeof stackedRounded === 'undefined') {
stackedRounded = false;
}
if (!vm.horizontal) {
// bar
left = vm.x - vm.width / 2;
right = vm.x + vm.width / 2;
top = vm.y;
bottom = vm.base;
borderSkipped = vm.borderSkipped || 'bottom';
} else {
// horizontal bar
left = vm.base;
right = vm.x;
top = vm.y - vm.height / 2;
bottom = vm.y + vm.height / 2;
borderSkipped = vm.borderSkipped || 'left';
}
ctx.beginPath();
ctx.fillStyle = vm.backgroundColor;
ctx.strokeStyle = vm.borderColor;
// Corner points, from bottom-left to bottom-right clockwise
// | 1 2 |
// | 0 3 |
var corners = [
[left, bottom],
[left, top],
[right, top],
[right, bottom]
];
// Find first (starting) corner with fallback to 'bottom'
var borders = ['bottom', 'left', 'top', 'right'];
var startCorner = borders.indexOf(borderSkipped, 0);
if (startCorner === -1) {
startCorner = 0;
}
function cornerAt(index) {
return corners[(startCorner + index) % 4];
}
// Draw rectangle from 'startCorner'
var corner = cornerAt(0);
ctx.moveTo(corner[0], corner[1]);
var nextCornerId, width, height, x, y;
for (var i = 1; i < 4; i++) {
corner = cornerAt(i);
nextCornerId = i + 1;
if (nextCornerId === 4) {
nextCornerId = 0;
}
width = corners[2][0] - corners[1][0];
height = corners[0][1] - corners[1][1];
x = corners[1][0];
y = corners[1][1];
radius = cornerRadius;
// Fix radius being too large
if (radius > Math.abs(height) / 2) {
radius = Math.floor(Math.abs(height) / 2);
}
if (radius > Math.abs(width) / 2) {
radius = Math.floor(Math.abs(width) / 2);
}
var xTL, xTR, yTL, yTR, xBL, xBR, yBL, yBR;
if (width < 0) {
// Negative values in a horizontal bar chart
xTL = x + width;
xTR = x;
yTL = y;
yTR = y;
xBL = x + width;
xBR = x;
yBL = y + height;
yBR = y + height;
// Draw
ctx.moveTo(xBL + radius, yBL);
ctx.lineTo(xBR - radius, yBR);
// Bottom right corner
fullCornerRadius ? ctx.quadraticCurveTo(xBR, yBR, xBR, yBR - radius) : ctx.lineTo(xBR, yBR, xBR, yBR - radius);
ctx.lineTo(xTR, yTR + radius);
// top right Corner
fullCornerRadius ? ctx.quadraticCurveTo(xTR, yTR, xTR - radius, yTR) : ctx.lineTo(xTR, yTR, xTR - radius, yTR);
ctx.lineTo(xTL + radius, yTL);
// top left corner
ctx.quadraticCurveTo(xTL, yTL, xTL, yTL + radius);
ctx.lineTo(xBL, yBL - radius);
// bttom left corner
ctx.quadraticCurveTo(xBL, yBL, xBL + radius, yBL);
} else {
var lastVisible = 0;
for (var findLast = 0, findLastTo = this._chart.data.datasets.length; findLast < findLastTo; findLast++) {
if (!this._chart.getDatasetMeta(findLast).hidden) {
lastVisible = findLast;
}
}
var rounded = this._datasetIndex === lastVisible;
if (rounded) {
//Positive Value
ctx.moveTo(x + radius, y);
ctx.lineTo(x + width - radius, y);
// top right
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
ctx.lineTo(x + width, y + height - radius);
// bottom right
if (fullCornerRadius || typeOfChart === 'horizontalBar') {
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
} else {
ctx.lineTo(x + width, y + height, x + width - radius, y + height);
}
ctx.lineTo(x + radius, y + height);
// bottom left
if (fullCornerRadius) {
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
} else {
ctx.lineTo(x, y + height, x, y + height - radius);
}
ctx.lineTo(x, y + radius);
// top left
if (fullCornerRadius || typeOfChart === 'bar') {
ctx.quadraticCurveTo(x, y, x + radius, y);
} else {
ctx.lineTo(x, y, x + radius, y);
}
} else {
ctx.moveTo(x, y);
ctx.lineTo(x + width, y);
ctx.lineTo(x + width, y + height);
ctx.lineTo(x, y + height);
ctx.lineTo(x, y);
}
}
}
ctx.fill();
};
export default class PaidMix extends Component {
@service dashboardStats;
/**
* Call this method when you need to fetch new data from the server.
*/
@action
loadCharts() {
this.dashboardStats.loadMemberCountStats();
// The dashboard stats service will take care or reusing and limiting API-requests between charts
if (this.mode === 'cadence') {
this.dashboardStats.loadPaidMembersByCadence();
} else {
this.dashboardStats.loadPaidMembersByTier();
}
}
@tracked mode = 'cadence';
modeOptions = MODE_OPTIONS;
get selectedModeOption() {
return this.modeOptions.find(option => option.value === this.mode);
}
get hasMultipleTiers() {
return this.dashboardStats.siteStatus?.hasMultipleTiers;
}
get totalMembers() {
return this.dashboardStats.memberCounts?.total ?? 0;
}
get isTotalMembersZero() {
return this.dashboardStats.memberCounts?.total === 0;
}
@action
onSwitchMode(selected) {
this.mode = selected.value;
if (this.loading) {
// We don't have the data yet for the newly selected mode
this.loadCharts();
}
}
get loading() {
if (this.mode === 'cadence') {
return this.dashboardStats.paidMembersByCadence === null;
}
return this.dashboardStats.paidMembersByTier === null;
}
get chartType() {
return 'horizontalBar';
}
get areTiersAllZero() {
if (this.dashboardStats.paidMembersByTier === null || this.dashboardStats.paidMembersByTier.length === 0) {
return true;
}
const data = this.dashboardStats.paidMembersByTier.map(stat => stat.members);
let areAllTiersZero = true;
for (let i = 0; i < data.length; i++) {
if (data[i] > 0) {
areAllTiersZero = false;
}
}
return areAllTiersZero;
}
get chartData() {
const totalCadence = this.dashboardStats.paidMembersByCadence.month + this.dashboardStats.paidMembersByCadence.year;
const monthlyPercentage = Math.round(this.dashboardStats.paidMembersByCadence.month / totalCadence * 100);
const annualPercentage = Math.round(this.dashboardStats.paidMembersByCadence.year / totalCadence * 100);
const barThickness = 5;
if (this.mode === 'cadence') {
// there has to be negative values to make rounded corners work
// empty chart render when there is no data
if (totalCadence === 0 && !this.isTotalMembersZero) {
return {
labels: ['Cadence'],
datasets: [{
label: 'Monthly',
data: [-50],
backgroundColor: '#F3F6F8',
barThickness
},{
label: 'Annual',
data: [50],
backgroundColor: '#EBEEF0',
barThickness
}]
};
// fake colorful data for underneath empty state
} else if (this.isTotalMembersZero) {
return {
labels: ['Cadence'],
datasets: [{
label: 'Monthly',
data: [-40],
backgroundColor: '#8E42FF',
barThickness
},{
label: 'Annual',
data: [60],
backgroundColor: '#FB76B4',
barThickness
}]
};
}
return {
labels: ['Cadence'],
datasets: [{
label: 'Monthly',
data: [-monthlyPercentage],
backgroundColor: '#8E42FF',
barThickness
}, {
label: 'Annual',
data: [annualPercentage],
backgroundColor: '#FB76B4',
barThickness
}]
};
}
// if it's for tiers...
const labels = this.dashboardStats.paidMembersByTier.map(stat => stat.tier.name);
const data = this.dashboardStats.paidMembersByTier.map(stat => stat.members);
const colors = ['#853EED', '#CA3FED', '#E993CC', '#DB7777', '#EE9696', '#FEC7C0', '#853EED', '#CA3FED', '#E993CC', '#DB7777', '#EE9696', '#FEC7C0'];
const zeroColors = ['#E6E9EB', '#EEF1F2', '#F6F8FA', '#EEF1F2', '#E6E9EB', '#EEF1F2', '#F6F8FA', '#EEF1F2', '#E6E9EB', '#EEF1F2', '#F6F8FA', '#EEF1F2'];
let datasets = [];
let totalTiersAmount;
// tiers all have 0 data
if (this.areTiersAllZero) {
totalTiersAmount = 100;
let equalPercentageData = Math.round(100 / data.length);
for (let i = 0; i < data.length; i++) {
data[i] = equalPercentageData;
}
// tiers have good data
} else {
totalTiersAmount = 0;
for (let i = 0; i < data.length; i++) {
totalTiersAmount += data[i];
}
}
for (let i = 0; i < data.length; i++) {
if (!this.areTiersAllZero && data[i] === 0) {
// If not all tiers are zero, hide tiers with 0 members, because
// they are not visible in the graph and can break rounded corners
continue;
}
let tierPercentage = Math.round(data[i] / totalTiersAmount * 100);
// The first value has to be negative to make rounded corners work
if (i === 0) {
tierPercentage = -tierPercentage;
}
datasets.push({
data: [tierPercentage],
label: labels[i],
backgroundColor: this.areTiersAllZero ? zeroColors[i] : colors[i],
barThickness
});
}
return {
labels: ['Tiers'],
datasets
};
}
get chartOptions() {
let that = this;
let ticksY = {display: false};
let totalCadence = this.dashboardStats.paidMembersByCadence.month + this.dashboardStats.paidMembersByCadence.year;
let minTickValue = -(Math.round(this.dashboardStats.paidMembersByCadence.month / totalCadence * 100));
let maxTickValue = Math.round(this.dashboardStats.paidMembersByCadence.year / totalCadence * 100);
// this is for cadence...
if (this.mode === 'cadence') {
// for when it's empty
if (totalCadence === 0) {
minTickValue = -50;
maxTickValue = 50;
}
ticksY = {
display: false,
min: minTickValue,
max: maxTickValue
};
// this is for tiers...
} else {
if (!this.areTiersAllZero) {
let data = this.dashboardStats.paidMembersByTier.map(stat => stat.members);
let totalTiersAmount = 0;
for (let i = 0; i < data.length; i++) {
totalTiersAmount += data[i];
}
let negativeTierPercentage = Math.round(data[0] / totalTiersAmount * 100);
ticksY = {
display: false,
min: -negativeTierPercentage,
max: 100 - negativeTierPercentage // take the negative away from 100 to create a full width bar
};
}
}
return {
responsive: true,
maintainAspectRatio: false,
cornerRadius: 50,
fullCornerRadius: false,
legend: {
display: false
},
layout: {
padding: {
top: 72,
bottom: 0,
left: 0,
right: 4
}
},
animation: false,
responsiveAnimationDuration: 1,
hover: {
onHover: function (e) {
e.target.style.cursor = 'pointer';
}
},
tooltips: {
enabled: false,
intersect: false,
mode: 'single',
custom: function (tooltip) {
// get tooltip element
const tooltipEl = document.getElementById('gh-dashboard-mix-tooltip');
const chartContainerEl = tooltipEl.parentElement;
const chartWidth = chartContainerEl.offsetWidth;
const tooltipWidth = tooltipEl.offsetWidth;
// only show tooltip when active
if (tooltip.opacity === 0) {
tooltipEl.style.opacity = 0;
return;
}
let offsetX = 0;
if (that.mode === 'cadence') {
// these adjustments should match the special width and margin values in css
if (tooltip.x > (chartWidth * 0.69) - tooltipWidth) {
offsetX = tooltipWidth - 10;
}
offsetX -= (chartWidth * 0.30);
} else {
if (tooltip.x > chartWidth - tooltipWidth) {
offsetX = tooltipWidth - 10;
}
}
// update tooltip styles
tooltipEl.style.opacity = 1;
tooltipEl.style.position = 'absolute';
tooltipEl.style.left = tooltip.x - offsetX + 'px';
tooltipEl.style.top = '30px';
},
callbacks: {
label: (tooltipItems, data) => {
const tooltipTextEl = document.querySelector('#gh-dashboard-mix-tooltip .gh-dashboard-tooltip-value');
const label = data.datasets[tooltipItems.datasetIndex].label || '';
var value = data.datasets[tooltipItems.datasetIndex].data[tooltipItems.index] || 0;
if (value < 0) {
value = -value;
}
if (that.isTotalMembersZero || totalCadence === 0) {
value = 0;
} else {
value += '%';
}
tooltipTextEl.innerHTML = `<span class="indicator solid" style="background-color: ${data.datasets[tooltipItems.datasetIndex].backgroundColor}"></span><span class="value">${value}</span><span class="metric">${label}</span>`;
},
title: () => {
return null;
}
}
},
scales: {
yAxes: [{
stacked: true,
gridLines: {
display: false
},
ticks: {
display: false
}
}],
xAxes: [{
stacked: true,
gridLines: {
display: false
},
ticks: ticksY
}]
}
};
}
get isChartCadence() {
return (this.mode === 'cadence');
}
get isChartTiers() {
return (this.mode === 'tiers');
}
}

View File

@ -0,0 +1,39 @@
<div class="gh-dashboard-minichart gh-dashboard-mrr">
<div class="gh-dashboard-content">
<div class="gh-dashboard-data">
<Dashboard::Parts::Metric
@label={{this.chartTitle}}
@value="{{this.currentMRRFormatted}}"
@trends={{this.hasTrends}}
@percentage={{this.mrrTrend}} />
</div>
<div class="gh-dashboard-chart">
{{#if this.loading}}
<div class="gh-dashboard-chart-loading">
<div class="gh-loading-spinner"></div>
</div>
{{else}}
<div class="gh-dashboard-chart-container">
<div class="gh-dashboard-chart-box">
<EmberChart
@type={{this.chartType}}
@data={{this.chartData}}
@options={{this.chartOptions}}
@height={{110}} />
</div>
<div id="gh-dashboard-mrr-tooltip" class="gh-dashboard-tooltip">
<div class="gh-dashboard-tooltip-label">
-
</div>
<div class="gh-dashboard-tooltip-value">
<span class="indicator line"></span>
<span class="value">-</span>
<span class="metric">MRR</span>
</div>
</div>
</div>
{{/if}}
</div>
</div>
</div>

View File

@ -0,0 +1,477 @@
/* global Chart */
import Component from '@glimmer/component';
import moment from 'moment';
import {getSymbol} from 'ghost-admin/utils/currency';
import {ghPriceAmount} from '../../../helpers/gh-price-amount';
import {inject as service} from '@ember/service';
const DATE_FORMAT = 'D MMM, YYYY';
// custom ChartJS draw function
Chart.defaults.hoverLine = Chart.defaults.line;
Chart.controllers.hoverLine = Chart.controllers.line.extend({
draw: function (ease) {
Chart.controllers.line.prototype.draw.call(this, ease);
if (this.chart.tooltip._active && this.chart.tooltip._active.length) {
let activePoint = this.chart.tooltip._active[0],
ctx = this.chart.ctx,
x = activePoint.tooltipPosition().x,
topY = this.chart.legend.bottom,
bottomY = this.chart.chartArea.bottom;
// draw line
ctx.save();
ctx.beginPath();
ctx.moveTo(x, topY);
ctx.lineTo(x, bottomY);
ctx.setLineDash([3, 4]);
ctx.lineWidth = 1;
ctx.strokeStyle = '#7C8B9A';
ctx.stroke();
ctx.restore();
}
}
});
export default class PaidMrr extends Component {
@service dashboardStats;
@service feature;
get loading() {
return this.dashboardStats.mrrStats === null;
}
get currentMRR() {
return this.dashboardStats.currentMRR ?? 0;
}
get mrrTrend() {
return this.calculatePercentage(this.dashboardStats.currentMRRTrend, this.dashboardStats.currentMRR);
}
get hasTrends() {
return this.dashboardStats.currentMRR !== null
&& this.dashboardStats.currentMRRTrend !== null;
}
get chartTitle() {
return 'MRR';
}
get chartType() {
return 'hoverLine'; // uses custom ChartJS draw function
}
get totalMembers() {
return this.dashboardStats.memberCounts?.total ?? 0;
}
get isTotalMembersZero() {
return this.dashboardStats.memberCounts && this.totalMembers === 0;
}
get mrrCurrencySymbol() {
if (this.dashboardStats.mrrStats === null) {
return '';
}
const firstCurrency = this.dashboardStats.mrrStats[0] ? this.dashboardStats.mrrStats[0].currency : 'usd';
return getSymbol(firstCurrency);
}
get currentMRRFormatted() {
// fake empty data
if (this.isTotalMembersZero) {
return '$123';
}
if (this.dashboardStats.mrrStats === null) {
return '-';
}
const valueText = ghPriceAmount(this.currentMRR, {cents: false});
return `${this.mrrCurrencySymbol}${valueText}`;
}
get chartData() {
let stats = this.dashboardStats.filledMrrStats;
let labels = stats.map(stat => stat.date);
let data = stats.map(stat => stat.mrr);
// with no members yet, let's show empty state with dummy data
if (this.isTotalMembersZero) {
stats = this.emptyData.stats;
labels = this.emptyData.labels;
data = this.emptyData.data;
}
// gradient for fill
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const gradient = ctx.createLinearGradient(0, 0, 0, 120);
gradient.addColorStop(0, 'rgba(143, 66, 255, 0.15');
gradient.addColorStop(1, 'rgba(143, 66, 255, 0.0');
return {
labels: labels,
datasets: [{
data: data,
tension: 1,
cubicInterpolationMode: 'monotone',
fill: false,
fillColor: gradient,
backgroundColor: gradient,
pointRadius: 0,
pointHitRadius: 10,
pointBorderColor: '#8E42FF',
pointBackgroundColor: '#8E42FF',
pointHoverBackgroundColor: '#8E42FF',
pointHoverBorderColor: '#8E42FF',
pointHoverRadius: 0,
borderColor: '#8E42FF',
borderJoinStyle: 'miter'
}]
};
}
get chartOptions() {
const that = this;
const barColor = this.feature.nightShift ? 'rgba(200, 204, 217, 0.25)' : 'rgba(200, 204, 217, 0.65)';
return {
responsive: true,
maintainAspectRatio: false,
title: {
display: false
},
legend: {
display: false
},
layout: {
padding: {
top: 2,
bottom: 2,
left: 0,
right: 0
}
},
hover: {
onHover: function (e) {
e.target.style.cursor = 'pointer';
}
},
animation: false,
responsiveAnimationDuration: 1,
tooltips: {
enabled: false,
intersect: false,
mode: 'index',
custom: function (tooltip) {
// get tooltip element
const tooltipEl = document.getElementById('gh-dashboard-mrr-tooltip');
const chartContainerEl = tooltipEl.parentElement;
const chartWidth = chartContainerEl.offsetWidth;
const tooltipWidth = tooltipEl.offsetWidth;
// only show tooltip when active
if (tooltip.opacity === 0) {
tooltipEl.style.opacity = 0;
return;
}
let offsetX = 0;
if (tooltip.x > chartWidth - tooltipWidth) {
offsetX = tooltipWidth - 10;
}
// update tooltip styles
tooltipEl.style.opacity = 1;
tooltipEl.style.position = 'absolute';
tooltipEl.style.left = tooltip.x - offsetX + 'px';
tooltipEl.style.top = tooltip.y + 'px';
},
callbacks: {
label: (tooltipItems, data) => {
const value = `${that.mrrCurrencySymbol}${ghPriceAmount(data.datasets[tooltipItems.datasetIndex].data[tooltipItems.index], {cents: false})}`;
document.querySelector('#gh-dashboard-mrr-tooltip .gh-dashboard-tooltip-value .value').innerHTML = value;
},
title: (tooltipItems) => {
const value = moment(tooltipItems[0].xLabel).format(DATE_FORMAT);
document.querySelector('#gh-dashboard-mrr-tooltip .gh-dashboard-tooltip-label').innerHTML = value;
}
}
},
scales: {
yAxes: [{
display: true,
gridLines: {
drawTicks: false,
display: false,
drawBorder: false,
color: 'transparent',
zeroLineColor: barColor,
zeroLineWidth: 1
},
ticks: {
display: false
}
}],
xAxes: [{
display: true,
scaleLabel: {
align: 'start'
},
gridLines: {
color: barColor,
borderDash: [4,4],
display: false,
drawBorder: true,
drawTicks: false,
zeroLineWidth: 1,
zeroLineColor: barColor,
zeroLineBorderDash: [4,4]
},
ticks: {
display: false,
beginAtZero: true
}
}]
}
};
}
// used for empty state
get emptyData() {
return {
stats: [
{
date: '2022-04-07',
mrr: 0,
currency: 'usd'
},
{
date: '2022-04-08',
mrr: 0,
currency: 'usd'
},
{
date: '2022-04-09',
mrr: 1500,
currency: 'usd'
},
{
date: '2022-04-10',
mrr: 2000,
currency: 'usd'
},
{
date: '2022-04-11',
mrr: 4500,
currency: 'usd'
},
{
date: '2022-04-12',
mrr: 7500,
currency: 'usd'
},
{
date: '2022-04-13',
mrr: 11000,
currency: 'usd'
},
{
date: '2022-04-14',
mrr: 12500,
currency: 'usd'
},
{
date: '2022-04-15',
mrr: 14500,
currency: 'usd'
},
{
date: '2022-04-16',
mrr: 18000,
currency: 'usd'
},
{
date: '2022-04-17',
mrr: 21500,
currency: 'usd'
},
{
date: '2022-04-18',
mrr: 25000,
currency: 'usd'
},
{
date: '2022-04-19',
mrr: 28000,
currency: 'usd'
},
{
date: '2022-04-20',
mrr: 30000,
currency: 'usd'
},
{
date: '2022-04-21',
mrr: 34000,
currency: 'usd'
},
{
date: '2022-04-22',
mrr: 35000,
currency: 'usd'
},
{
date: '2022-04-23',
mrr: 35500,
currency: 'usd'
},
{
date: '2022-04-24',
mrr: 37000,
currency: 'usd'
},
{
date: '2022-04-25',
mrr: 38000,
currency: 'usd'
},
{
date: '2022-04-26',
mrr: 40500,
currency: 'usd'
},
{
date: '2022-04-27',
mrr: 43500,
currency: 'usd'
},
{
date: '2022-04-28',
mrr: 47000,
currency: 'usd'
},
{
date: '2022-04-29',
mrr: 48000,
currency: 'usd'
},
{
date: '2022-04-30',
mrr: 50500,
currency: 'usd'
},
{
date: '2022-05-01',
mrr: 53500,
currency: 'usd'
},
{
date: '2022-05-02',
mrr: 55000,
currency: 'usd'
},
{
date: '2022-05-03',
mrr: 56500,
currency: 'usd'
},
{
date: '2022-05-04',
mrr: 57000,
currency: 'usd'
},
{
date: '2022-05-05',
mrr: 58000,
currency: 'usd'
},
{
date: '2022-05-06',
mrr: 58500,
currency: 'usd'
}
],
labels: [
'2022-04-07',
'2022-04-08',
'2022-04-09',
'2022-04-10',
'2022-04-11',
'2022-04-12',
'2022-04-13',
'2022-04-14',
'2022-04-15',
'2022-04-16',
'2022-04-17',
'2022-04-18',
'2022-04-19',
'2022-04-20',
'2022-04-21',
'2022-04-22',
'2022-04-23',
'2022-04-24',
'2022-04-25',
'2022-04-26',
'2022-04-27',
'2022-04-28',
'2022-04-29',
'2022-04-30',
'2022-05-01',
'2022-05-02',
'2022-05-03',
'2022-05-04',
'2022-05-05',
'2022-05-06'
],
data: [
0,
1500,
4000,
5000,
9000,
11500,
22500,
26000,
30000,
30000,
31000,
33000,
33500,
35500,
36500,
36500,
40000,
40500,
43500,
47000,
49000,
49500,
50000,
50000,
53000,
56000,
58000,
61000,
63500,
63500
]
};
}
calculatePercentage(from, to) {
if (from === 0) {
if (to > 0) {
return 100;
}
return 0;
}
return Math.round((to - from) / from * 100);
}
}

View File

@ -0,0 +1,131 @@
<section class="gh-dashboard-section gh-dashboard-recents" {{did-insert this.loadPosts}}>
<article class="gh-dashboard-box">
<div class="gh-dashboard-tabs">
<button type="button" class="gh-dashboard-tab {{if this.postsTabSelected 'is-selected'}}" {{on "click" this.changeTabToPosts}}>
<Dashboard::Parts::Metric
@label="Recent posts" />
</button>
{{#if this.areMembersEnabled}}
<button type="button" class="gh-dashboard-tab {{if this.activityTabSelected 'is-selected'}}" {{on "click" this.changeTabToActivity}}>
<Dashboard::Parts::Metric
@label="Member activity" />
</button>
{{/if}}
</div>
{{#if this.postsTabSelected}}
<div class="gh-dashboard-recents-posts gh-dashboard-list {{unless this.areNewslettersEnabled 'is-single'}}">
<div class="gh-dashboard-list-header">
<div class="gh-dashboard-list-title">Title</div>
{{#if this.areNewslettersEnabled}}
<div class="gh-dashboard-list-title">Sends</div>
<div class="gh-dashboard-list-title">Open rate</div>
{{else}}
<div class="gh-dashboard-list-title">Published</div>
{{/if}}
</div>
<div class="gh-dashboard-list-body">
{{#each this.posts as |post|}}
<LinkTo class="gh-dashboard-list-item permalink" @route="editor.edit" @models={{array post.displayName post.id}}>
<div class="gh-dashboard-list-item-sub">
<span class="gh-dashboard-list-text">{{post.title}}</span>
</div>
{{#if this.areNewslettersEnabled}}
<div class="gh-dashboard-list-item-sub">
<span class="gh-dashboard-metric-minivalue {{unless post.email "na"}}">
{{#if post.email}}
{{format-number post.email.emailCount}}
{{else}}
&mdash;
{{/if}}
</span>
</div>
<div class="gh-dashboard-list-item-sub">
<span class="gh-dashboard-rate-bar">
{{#if post.email}}
<span class="gh-dashboard-metric-minivalue">{{post.email.openRate}}%</span>
<span class="gh-dashboard-rate-amount"><span style={{html-safe (concat "width: " post.email.openRate "%;")}}/></span>
{{else}}
<span class="gh-dashboard-metric-minivalue na">&mdash;</span>
{{/if}}
</span>
</div>
{{else}}
<div class="gh-dashboard-list-item-sub">
<span class="gh-dashboard-list-subtext">{{moment-format (moment-site-tz post.publishedAtUTC) "DD MMM YYYY HH:mm"}}</span>
</div>
{{/if}}
</LinkTo>
{{else}}
<div class="gh-dashboard-list-empty">
<p>No published posts yet.</p>
</div>
{{/each}}
</div>
<div class="gh-dashboard-list-footer">
<LinkTo @route="posts" @query={{reset-query-params "posts"}}>See all posts &rarr;</LinkTo>
</div>
</div>
{{else}}
<div class="gh-dashboard-recents-activity gh-dashboard-list" data-test-dashboard-member-activity>
<div class="gh-dashboard-list-header">
<div class="gh-dashboard-list-title">Member</div>
<div class="gh-dashboard-list-title">Event</div>
<div class="gh-dashboard-list-title">Time</div>
</div>
<div class="gh-dashboard-list-body">
{{#let (members-event-fetcher filter=(members-event-filter excludeEmailEvents=true) pageSize=5) as |eventsFetcher|}}
{{#if eventsFetcher.isError}}
<div class="gh-dashboard-list-error">
<p>There was an error loading events</p>
{{#if eventsFetcher.errorMessage}}
<code>{{eventsFetcher.errorMessage}}</code>
{{/if}}
</div>
{{/if}}
{{#if eventsFetcher.isLoading}}
<div class="gh-dashboard-list-loading">
<div class="gh-loading-spinner"></div>
</div>
{{else}}
{{#if eventsFetcher.data}}
{{#each eventsFetcher.data as |event|}}
{{#let (parse-member-event event eventsFetcher.hasMultipleNewsletters) as |parsedEvent|}}
<div class="gh-dashboard-list-item member-details">
<div class="gh-dashboard-list-item-sub">
<GhMemberAvatar @member={{parsedEvent.member}} @containerClass="w8 h8 mr3 flex-shrink-0" />
<LinkTo class="gh-dashboard-list-text" @route="member" @model="{{parsedEvent.memberId}}" data-test-dashboard-member-activity-item>{{parsedEvent.subject}}</LinkTo>
</div>
<div class="gh-dashboard-list-item-sub">
{{svg-jar parsedEvent.icon}}
<span class="gh-dashboard-list-subtext">
{{capitalize-first-letter parsedEvent.action}}
{{#if parsedEvent.url}}
"<a class="ghost-members-activity-object-link" href="{{parsedEvent.url}}" target="_blank" rel="noopener noreferrer">{{parsedEvent.object}}</a>"
{{else}}
{{parsedEvent.object}}
{{/if}}
{{parsedEvent.info}}
</span>
</div>
<div class="gh-dashboard-list-item-sub">
<span class="gh-dashboard-list-subtext">{{moment-format parsedEvent.timestamp "DD MMM YYYY HH:mm"}}</span>
</div>
</div>
{{/let}}
{{/each}}
{{else}}
<div class="gh-dashboard-list-empty" data-test-no-member-activities>
<p>No activity yet.</p>
</div>
{{/if}}
{{/if}}
{{/let}}
</div>
<div class="gh-dashboard-list-footer">
<LinkTo @route="members-activity" @query={{reset-query-params "members-activity"}}>See all activity &rarr;</LinkTo>
</div>
</div>
{{/if}}
</article>
</section>

View File

@ -0,0 +1,43 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
import {tracked} from '@glimmer/tracking';
export default class Recents extends Component {
@service store;
@service dashboardStats;
@tracked selected = 'posts';
@tracked posts = [];
@action
async loadPosts() {
this.posts = await this.store.query('post', {limit: 5, filter: 'status:[published,sent]', order: 'published_at desc'});
}
@action
changeTabToPosts() {
this.selected = 'posts';
}
@action
changeTabToActivity() {
this.selected = 'activity';
}
get postsTabSelected() {
return (this.selected === 'posts');
}
get activityTabSelected() {
return (this.selected === 'activity');
}
get areMembersEnabled() {
return this.dashboardStats.siteStatus?.membersEnabled;
}
get areNewslettersEnabled() {
return this.dashboardStats.siteStatus?.newslettersEnabled;
}
}

View File

@ -0,0 +1,48 @@
<div class="gh-dashboard-metric {{if @center "is-center"}} {{if @reverse "is-reverse"}} {{if @large "is-large"}}">
<div class="gh-dashboard-metric-data">
{{#if @secondary}}
{{#if @value}}
<div class="gh-dashboard-metric-value {{if @secondary 'is-secondary'}}">
<span class="value">{{@value}}</span>
{{#if @trends}}
<Dashboard::Parts::Percentage @percentage={{@percentage}}/>
{{/if}}
</div>
{{/if}}
<h5 class="gh-dashboard-metric-label {{if @secondary 'is-secondary'}}">
{{@label}}
</h5>
{{else}}
{{#if @reverse}}
{{#if @value}}
<div class="gh-dashboard-metric-value">
<span class="value">{{@value}}</span>
{{#if @trends}}
<Dashboard::Parts::Percentage @percentage={{@percentage}}/>
{{/if}}
</div>
{{/if}}
<h5 class="gh-dashboard-metric-label">
{{@label}}
</h5>
{{else}}
<h5 class="gh-dashboard-metric-label">
{{@label}}
</h5>
{{#if @value}}
<div class="gh-dashboard-metric-value">
<span class="value">{{@value}}</span>
{{#if @trends}}
<Dashboard::Parts::Percentage @percentage={{@percentage}}/>
{{/if}}
</div>
{{/if}}
{{/if}}
{{/if}}
</div>
{{#if @extra}}
<div class="gh-dashboard-metric-extra">
{{@extra}}
</div>
{{/if}}
</div>

View File

@ -0,0 +1,7 @@
{{#if (gt @percentage 0) }}
<div class="gh-dashboard-percentage is-positive">+{{ @percentage }}%</div>
{{else if (lt @percentage 0)}}
<div class="gh-dashboard-percentage is-negative">{{ @percentage }}%</div>
{{else}}
<div class="gh-dashboard-percentage">0%</div>
{{/if}}

View File

@ -0,0 +1,7 @@
<div class="gh-dashboard-zero">
<div class="gh-dashboard-zero-message">
<h4>Welcome to your Dashboard</h4>
<p>You'll find member analytics here once<br />someone signs up.</p>
<p><LinkTo @route="members">Add or import members &rarr;</LinkTo></p>
</div>
</div>

View File

@ -0,0 +1,120 @@
<div class="gh-main-section prototype-control-panel" {{did-insert this.onInsert}}>
<h4 class="gh-main-section-header small bn">Prototype control panel</h4>
<div class="gh-expandable">
<div class="gh-expandable-block">
<div class="gh-expandable-header">
<div>
<h4 class="gh-expandable-title">Enable fake data</h4>
<p class="gh-expandable-description">
Replace real data with mocked data to test the dashboard
</p>
</div>
<div class="for-switch">
<label class="switch">
<Input @type="checkbox" @checked={{this.enabled}} />
<span class="input-toggle-component"></span>
</label>
</div>
</div>
</div>
{{#if this.enabled}}
<div class="gh-expandable-block">
<div class="gh-expandable-header">
<div>
<h4 class="gh-expandable-title">Emulated state</h4>
<p class="gh-expandable-description">
Choose a state you want to emulate for this site
</p>
</div>
<div>
<PowerSelect
@selected={{this.selectedStateOption}}
@options={{this.stateOptions}}
@searchEnabled={{false}}
@onChange={{this.onStateChange}}
@triggerComponent="gh-power-select/trigger"
@triggerClass="gh-contentfilter-menu-trigger"
@dropdownClass="gh-contentfilter-menu-dropdown"
@matchTriggerWidth={{false}}
as |option|
>
{{#if option.name}}{{option.name}}{{else}}<span class="red">Unknown option</span>{{/if}}
</PowerSelect>
</div>
</div>
</div>
<div class="gh-expandable-block">
<div class="gh-expandable-header">
<div>
<h4 class="gh-expandable-title">Newsletters enabled</h4>
<p class="gh-expandable-description">
All email related charts shown
</p>
</div>
<div class="for-switch">
<label class="switch">
<Input @type="checkbox" @checked={{this.mockNewslettersEnabled}} />
<span class="input-toggle-component"></span>
</label>
</div>
</div>
</div>
<div class="gh-expandable-block">
<div class="gh-expandable-header">
<div>
<h4 class="gh-expandable-title">Members enabled</h4>
<p class="gh-expandable-description">
Setting subscription access to other value than 'Nobody'
</p>
</div>
<div class="for-switch">
<label class="switch">
<Input @type="checkbox" @checked={{this.mockMembersEnabled}} />
<span class="input-toggle-component"></span>
</label>
</div>
</div>
</div>
{{#if this.mockMembersEnabled}}
<div class="gh-expandable-block">
<div class="gh-expandable-header">
<div>
<h4 class="gh-expandable-title">Has paid tiers and Stripe connected</h4>
<p class="gh-expandable-description">
Paid memberships
</p>
</div>
<div class="for-switch">
<label class="switch">
<Input @type="checkbox" @checked={{this.mockPaidTiers}} />
<span class="input-toggle-component"></span>
</label>
</div>
</div>
</div>
<div class="gh-expandable-block">
<div class="gh-expandable-header">
<div>
<h4 class="gh-expandable-title">Has multiple paid tiers</h4>
<p class="gh-expandable-description">
Adds the ability to show paid mix per tier
</p>
</div>
<div class="for-switch">
<label class="switch">
<Input @type="checkbox" @checked={{this.mockMultipleTiers}} />
<span class="input-toggle-component"></span>
</label>
</div>
</div>
</div>
{{/if}}
{{/if}}
</div>
</div>

View File

@ -0,0 +1,170 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
import {tracked} from '@glimmer/tracking';
const STATE_OPTIONS = [
{
name: 'No data',
value: {
days: 0
}
},
{
name: '7 days',
value: {
days: 7
}
},
{
name: '30 days',
value: {
days: 30
}
},
{
name: '90 days',
value: {
days: 90
}
},
{
name: 'One year',
value: {
days: 365
}
},
{
name: 'Two years',
value: {
days: 730
}
}
];
export default class ControlPanel extends Component {
@service dashboardStats;
@service dashboardMocks;
stateOptions = STATE_OPTIONS;
@tracked state = STATE_OPTIONS[1].value;
@action
onInsert() {
this.loadState();
this.dashboardMocks.updateMockedData(this.state);
// Don't reload all (because then we would load unused graphs too)
if (this.enabled) {
this.updateState();
}
}
saveState() {
try {
localStorage.setItem('dashboard5-prototype-state', JSON.stringify(this.state));
localStorage.setItem('dashboard5-prototype-status', JSON.stringify(this.dashboardMocks.siteStatus));
localStorage.setItem('dashboard5-prototype-enabled', JSON.stringify(this.enabled));
} catch (e) {
// ignore localStorage not supported errors
}
}
loadState() {
try {
const savedState = localStorage.getItem('dashboard5-prototype-state');
if (savedState) {
const parsed = JSON.parse(savedState);
if (parsed) {
this.state = parsed;
}
}
const savedStatus = localStorage.getItem('dashboard5-prototype-status');
if (savedStatus) {
const parsed = JSON.parse(savedStatus);
if (parsed) {
this.dashboardMocks.siteStatus = {...this.dashboardMocks.siteStatus, ...parsed};
}
}
const enabledStr = localStorage.getItem('dashboard5-prototype-enabled');
if (enabledStr) {
const parsed = JSON.parse(enabledStr);
if (typeof parsed === 'boolean' && parsed !== this.dashboardMocks.enabled) {
this.dashboardMocks.enabled = parsed;
}
}
} catch (e) {
// ignore localStorage not supported errors
}
}
get selectedStateOption() {
return this.stateOptions.find(option => option.value.days === this.state.days);
}
@action
onStateChange(option) {
this.state = option.value;
this.updateMockedData();
this.updateState();
}
@action
updateMockedData() {
this.dashboardMocks.updateMockedData(this.state);
}
updateState() {
this.dashboardStats.reloadAll();
this.saveState();
}
// Convenience mappers
get enabled() {
return this.dashboardMocks.enabled;
}
set enabled(val) {
this.dashboardMocks.enabled = val;
this.updateState();
}
get mockPaidTiers() {
return this.dashboardMocks.siteStatus?.hasPaidTiers;
}
set mockPaidTiers(val) {
this.dashboardMocks.siteStatus.hasPaidTiers = val;
this.updateState();
}
get mockMultipleTiers() {
return this.dashboardMocks.siteStatus?.hasMultipleTiers;
}
set mockMultipleTiers(val) {
this.dashboardMocks.siteStatus.hasMultipleTiers = val;
this.updateState();
}
get mockNewslettersEnabled() {
return this.dashboardMocks.siteStatus?.newslettersEnabled;
}
set mockNewslettersEnabled(val) {
this.dashboardMocks.siteStatus.newslettersEnabled = val;
this.updateState();
}
get mockMembersEnabled() {
return this.dashboardMocks.siteStatus?.membersEnabled;
}
set mockMembersEnabled(val) {
this.dashboardMocks.siteStatus.membersEnabled = val;
this.updateState();
}
}

View File

@ -0,0 +1,17 @@
<section class="gh-dashboard-resource gh-dashboard-community">
<a href="https://ghost.org/resources/community/" target="_blank" rel="noopener noreferrer" class="gh-dashboard-resource-box">
<div class="gh-dashboard-resource-title">
<h4>Ghost Creator Community</h4>
</div>
<div class="gh-dashboard-resource-body">
<div class="gh-dashboard-list">
<div class="gh-dashboard-list-body">
<p>Talk strategy.<br />Get advice.<br />Or just hang out.</p>
</div>
</div>
</div>
<div class="gh-dashboard-resource-footer">
Share the journey &rarr;
</div>
</a>
</section>

View File

@ -0,0 +1,24 @@
<section class="gh-dashboard-resource gh-dashboard-newsletter" {{did-insert this.load}}>
<article class="gh-dashboard-resource-box">
<div class="gh-dashboard-resource-title">
<h4>Latest from the newsletter</h4>
</div>
<div class="gh-dashboard-resource-body">
{{#if (not (or this.loading this.error))}}
<div class="gh-dashboard-resource-bigarticle">
{{#each this.newsletters as |entry|}}
<a class="gh-dashboard-resource-smallarticle" href={{set-query-params entry.url utm_source='admin'}} target="_blank" rel="noopener noreferrer">
<div class="gh-dashboard-resource-text">
<h3>{{entry.title}}</h3>
<p>{{entry.excerpt}}</p>
</div>
</a>
{{/each}}
</div>
{{/if}}
</div>
<div class="gh-dashboard-resource-footer">
<a href="https://ghost.org/resources/newsletter/" target="_blank" class="gh-dashboard-subscribe-button" rel="noopener noreferrer">Subscribe&nbsp;<span>to the newsletter&nbsp;</span>&rarr;</a>
</div>
</article>
</section>

View File

@ -0,0 +1,44 @@
import Component from '@glimmer/component';
import fetch from 'fetch';
import {action} from '@ember/object';
import {task} from 'ember-concurrency';
import {tracked} from '@glimmer/tracking';
const API_URL = 'https://resources.ghost.io/resources';
const API_KEY = 'b30afc1721f5d8d021ec3450ef';
const NEWSLETTER_COUNT = 1;
export default class Newsletter extends Component {
@tracked loading = null;
@tracked error = null;
@tracked newsletters = null;
@action
load() {
this.loading = true;
this.fetch.perform().then(() => {
this.loading = false;
}).catch((error) => {
this.error = error;
this.loading = false;
});
}
@task
*fetch() {
const order = encodeURIComponent('published_at DESC');
const key = encodeURIComponent(API_KEY);
const limit = encodeURIComponent(NEWSLETTER_COUNT);
const filter = encodeURIComponent('tag:newsletter');
let response = yield fetch(`${API_URL}/ghost/api/content/posts/?limit=${limit}&order=${order}&key=${key}&include=none&filter=${filter}`);
if (!response.ok) {
// eslint-disable-next-line
console.error('Failed to fetch newsletters', {response});
this.error = 'Failed to fetch';
return;
}
let result = yield response.json();
this.newsletters = result.posts || [];
}
}

View File

@ -0,0 +1,23 @@
<section class="gh-dashboard-resource gh-dashboard-resources" {{did-insert this.load}}>
<article class="gh-dashboard-resource-box">
{{#if (not (or this.loading this.error))}}
<a href="{{this.resource.url}}" target="_blank" class="gh-dashboard-resource-thumbnail" rel="noopener noreferrer" style={{html-safe (concat "background-image: url(" this.resource.feature_image ")")}} aria-label="Resource link"></a>
<div class="gh-dashboard-resource-contents">
<div class="gh-dashboard-resource-title">
<h4>Resources</h4>
</div>
<div class="gh-dashboard-resource-body">
<a href="{{this.resource.url}}" target="_blank" class="gh-dashboard-resource-bigarticle" rel="noopener noreferrer">
<div class="gh-dashboard-resource-text">
<h3>{{this.resource.title}}</h3>
<p>{{this.resource.excerpt}}</p>
</div>
</a>
</div>
<div class="gh-dashboard-resource-footer">
<a href="https://ghost.org/resources/" target="_blank" rel="noopener noreferrer">Learn more &rarr;</a>
</div>
</div>
{{/if}}
</article>
</section>

View File

@ -0,0 +1,76 @@
import Component from '@glimmer/component';
import fetch from 'fetch';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency';
import {tracked} from '@glimmer/tracking';
const API_URL = 'https://resources.ghost.io/resources';
const API_KEY = 'b30afc1721f5d8d021ec3450ef';
const RESOURCE_COUNT = 1;
export default class Resources extends Component {
@service dashboardStats;
@tracked loading = null;
@tracked error = null;
@tracked resources = null;
@tracked resource = null;
@action
load() {
this.loading = true;
this.fetch.perform().then(() => {
this.loading = false;
}).catch((error) => {
this.error = error;
this.loading = false;
});
}
get tag() {
// Depending on the state of the site, we might want to show resources from different tags.
if (this.areMembersEnabled && this.hasPaidTiers) {
return 'business';
}
if (this.areMembersEnabled) {
return 'growth';
}
return 'building';
}
@task
*fetch() {
const order = encodeURIComponent('published_at DESC');
const key = encodeURIComponent(API_KEY);
const limit = encodeURIComponent(RESOURCE_COUNT);
const filter = encodeURIComponent('tag:' + this.tag);
let response = yield fetch(`${API_URL}/ghost/api/content/posts/?limit=${limit}&order=${order}&key=${key}&include=none&filter=${filter}`);
if (!response.ok) {
// eslint-disable-next-line
console.error('Failed to fetch resources', {response});
this.error = 'Failed to fetch';
return;
}
let result = yield response.json();
this.resources = result.posts || [];
this.resource = this.resources[0]; // just get the first
}
get hasPaidTiers() {
return this.dashboardStats.siteStatus?.hasPaidTiers;
}
get areNewslettersEnabled() {
return this.dashboardStats.siteStatus?.newslettersEnabled;
}
get areMembersEnabled() {
return this.dashboardStats.siteStatus?.membersEnabled;
}
get hasNothingEnabled() {
return (!this.areMembersEnabled && !this.areNewslettersEnabled && !this.hasPaidTiers);
}
}

View File

@ -0,0 +1,33 @@
<section class="gh-dashboard-resource gh-dashboard-staff-picks" {{did-insert this.load}}>
<article class="gh-dashboard-resource-box">
<div class="gh-dashboard-resource-title is-large has-border">
<h4>Staff picks</h4>
<p>Hand picked stories from around the web, published with Ghost.</p>
</div>
<div class="gh-dashboard-resource-body">
<div class="gh-dashboard-list">
{{#if (not (or this.loading this.error))}}
<div class="gh-dashboard-list-body">
{{#each this.staffPicks as |entry|}}
<div class="gh-dashboard-list-item">
<a class="gh-dashboard-list-post permalink" href={{set-query-params entry.link utm_source='ghost'}} target="_blank" rel="noopener noreferrer">
<span class="gh-dashboard-list-link">
<span>{{entry.title}}</span>
</span>
<div class="gh-dashboard-resource-secondary">{{entry.creator}}</div>
</a>
</div>
{{else}}
<div class="gh-dashboard-list-empty">
<p>No staff picks yet.</p>
</div>
{{/each}}
</div>
{{/if}}
</div>
</div>
<div class="gh-dashboard-resource-footer">
<a href="https://twitter.com/ghoststaffpicks" target="_blank" rel="noopener noreferrer">{{svg-jar "twitter-logo"}} <span>Follow on Twitter</span></a>
</div>
</article>
</section>

View File

@ -0,0 +1,57 @@
import Component from '@glimmer/component';
import fetch from 'fetch';
import {action} from '@ember/object';
import {task} from 'ember-concurrency';
import {tracked} from '@glimmer/tracking';
const RSS_FEED_URL = 'https://zapier.com/engine/rss/678920/ghoststaffpicks';
const LIMIT = 3;
export default class StaffPicks extends Component {
@tracked loading = null;
@tracked error = null;
@tracked staffPicks = null;
@action
load() {
this.loading = true;
this.fetch.perform().then(() => {
this.loading = false;
}, (error) => {
this.error = error;
this.loading = false;
});
}
@task
*fetch() {
let response = yield fetch(RSS_FEED_URL);
if (!response.ok) {
// eslint-disable-next-line
console.error('Failed to fetch staff picks', {response});
this.error = 'Failed to fetch';
return;
}
const str = yield response.text();
const document = new DOMParser().parseFromString(str, 'text/xml');
const items = document.querySelectorAll('channel > item');
this.staffPicks = [];
for (let index = 0; index < items.length && index < LIMIT; index += 1) {
const item = items[index];
const title = item.getElementsByTagName('title')[0].textContent;
const link = item.getElementsByTagName('link')[0].textContent;
const creator = item.getElementsByTagName('dc:creator')[0].textContent;
const entry = {
title,
link,
creator
};
this.staffPicks.push(entry);
}
}
}

View File

@ -0,0 +1,33 @@
<section class="gh-dashboard-resource gh-dashboard-whats-new" {{did-insert this.load}}>
<article class="gh-dashboard-resource-box">
<div class="gh-dashboard-resource-title is-large has-border">
<h4>What's new</h4>
<p>All the latest improvements.</p>
</div>
<div class="gh-dashboard-resource-body">
<div class="gh-dashboard-list {{if this.whatsNew.hasNew "has-new"}}">
{{#if (not (or this.loading this.error))}}
<div class="gh-dashboard-list-body">
{{#each this.entries as |entry|}}
<div class="gh-dashboard-list-item">
<LinkTo class="gh-dashboard-list-post" @route="whatsnew" @query={{hash entry=entry.slug}}>
<span class="gh-dashboard-list-link">
<span>{{entry.title}}</span>
</span>
<div class="gh-dashboard-resource-secondary">{{moment-format entry.published_at "D MMM YYYY"}}</div>
</LinkTo>
</div>
{{else}}
<div class="gh-dashboard-list-empty">
<p>No new features yet.</p>
</div>
{{/each}}
</div>
{{/if}}
</div>
</div>
<div class="gh-dashboard-resource-footer">
<LinkTo @route="whatsnew" @query={{hash entry=null}} class="green">See more features &rarr;</LinkTo>
</div>
</article>
</section>

View File

@ -0,0 +1,24 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
import {tracked} from '@glimmer/tracking';
export default class WhatsNew extends Component {
@service whatsNew;
@tracked entries = null;
@tracked loading = null;
@tracked error = null;
@action
load() {
this.loading = true;
this.whatsNew.fetchLatest.perform().then(() => {
this.loading = false;
this.entries = this.whatsNew.entries.slice(0, 3);
}, (error) => {
this.error = error;
this.loading = false;
});
}
}

View File

@ -0,0 +1,84 @@
<div class="flex flex-column h-100">
<header class="modal-header gh-post-preview-header" data-test-modal="preview-email">
<div class="left">
<button class="gh-btn-editor gh-editor-back-button" title="Close" type="button" {{on "click" @close}}>
<span>{{svg-jar "arrow-left"}} Editor</span>
</button>
</div>
<div class="gh-post-preview-btn-group">
<div class="gh-contentfilter gh-btn-group">
<button type="button" class="gh-btn {{if (eq this.tab "browser") "gh-btn-group-selected"}} gh-post-preview-mode" {{on "click" (fn this.changeTab "browser")}}><span>{{svg-jar "desktop"}}</span></button>
<button type="button" class="gh-btn {{if (eq this.tab "mobile") "gh-btn-group-selected"}} gh-post-preview-mode" {{on "click" (fn this.changeTab "mobile")}}><span>{{svg-jar "mobile-phone"}}</span></button>
{{#if (and (not-eq this.settings.membersSignupAccess "none") (not-eq this.settings.editorDefaultEmailRecipients "disabled"))}}
{{#if (and @data.publishOptions.post.isPost (not @data.publishOptions.user.isContributor))}}
<button type="button" class="gh-btn {{if (eq this.tab "email") "gh-btn-group-selected"}} gh-post-preview-mode" {{on "click" (fn this.changeTab "email")}}><span>{{svg-jar "email-unread"}}</span></button>
{{/if}}
{{/if}}
<button type="button" class="gh-btn {{if (eq this.tab "social") "gh-btn-group-selected"}} gh-post-preview-mode" {{on "click" (fn this.changeTab "social")}}><span>{{svg-jar "twitter"}}</span></button>
</div>
</div>
<div class="right">
<button
type="button"
class="gh-btn gh-btn-editor gh-editor-preview-trigger active"
{{on "click" @close}}
>
<span>Preview</span>
</button>
{{#if @data.publishOptions.user.isContributor}}
<GhTaskButton
@buttonText="Save"
@task={{@data.saveTask}}
@runningText="Saving"
@class="gh-btn gh-btn-icon gh-btn-editor gh-editor-save-trigger contributor-save-button"
data-test-preview-contributor-save />
{{else}}
<button
type="button"
class="gh-btn gh-btn-editor darkgrey gh-publish-trigger"
{{on "click" @data.togglePreviewPublish}}
>
<span>Publish</span>
</button>
{{/if}}
<div class="settings-menu-toggle-spacer"></div>
</div>
</header>
{{#if this.saveFirstTask.isRunning}}
<GhLoadingSpinner />
{{else}}
{{#if (eq this.tab "browser")}}
<Editor::Modals::Preview::Browser
@post={{@data.publishOptions.post}}
@skipAnimation={{this.skipAnimation}}
/>
{{/if}}
{{#if (and (eq this.tab "mobile"))}}
<Editor::Modals::Preview::Mobile
@post={{@data.publishOptions.post}}
@skipAnimation={{this.skipAnimation}}
/>
{{/if}}
{{#unless @data.publishOptions.user.isContributor}}
{{#if (and (eq this.tab "email") @data.publishOptions.post.isPost)}}
<Editor::Modals::Preview::Email
@post={{@data.publishOptions.post}}
@newsletter={{@data.publishOptions.newsletter}}
@skipAnimation={{this.skipAnimation}}
/>
{{/if}}
{{/unless}}
{{#if (eq this.tab "social")}}
<Editor::Modals::Preview::Social
@post={{@data.publishOptions.post}}
@skipAnimation={{this.skipAnimation}}
/>
{{/if}}
{{/if}}
</div>

View File

@ -0,0 +1,48 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency';
import {tracked} from '@glimmer/tracking';
export default class EditorPostPreviewModal extends Component {
@service settings;
@service session;
static modalOptions = {
className: 'fullscreen-modal-total-overlay publish-modal',
omitBackdrop: true,
ignoreBackdropClick: true
};
@tracked tab = this.args.data.currentTab || 'browser';
@tracked isChangingTab = false;
constructor() {
super(...arguments);
this.saveFirstTask.perform();
}
get skipAnimation() {
return this.args.data.skipAnimation || this.isChangingTab;
}
@action
changeTab(tab) {
this.tab = tab;
this.isChangingTab = true;
this.args.data.changeTab?.(tab);
}
@task
*saveFirstTask() {
const {saveTask, publishOptions, hasDirtyAttributes} = this.args.data;
if (saveTask.isRunning) {
return yield saveTask.last;
}
if (publishOptions.post.isDraft && hasDirtyAttributes) {
yield saveTask.perform();
}
}
}

View File

@ -0,0 +1,30 @@
<div class="gh-post-preview-container gh-post-preview-browser-container {{unless @skipAnimation "fade-in"}}">
<div class="gh-browserpreview-browser">
<div class="tabs">
<ul><li></li><li></li><li></li></ul>
<div>
{{#if (or @icon this.settings.icon)}}
<span class="favicon"><img src={{or @icon this.settings.icon}} alt="icon"></span>
{{else}}
<span class="favicon default">{{svg-jar "default-favicon"}}</span>
{{/if}}
<span class="db truncate w-90">{{@post.previewUrl}}</span>
<button type="button" {{on "click" (perform this.copyPreviewUrl)}} class="gh-post-preview-url">
<span class="green-d1">
{{#if this.copyPreviewUrl.isRunning}}
{{svg-jar "check-circle" class="check v-mid mr1 ml2"}} Copied
{{else}}
{{svg-jar "copy" class="w4 ml3 v-mid fill-darkgrey"}}
{{/if}}
</span>
</button>
<a href={{@post.previewUrl}} target="_blank" rel="noopener noreferrer" class="gh-publish-preview-newtab">
{{svg-jar "external"}}
</a>
</div>
</div>
</div>
<div class="gh-browserpreview-iframecontainer">
<iframe class="gh-pe-iframe" src={{@post.previewUrl}} title="Desktop browser post preview"></iframe>
</div>
</div>

View File

@ -0,0 +1,11 @@
import Component from '@glimmer/component';
import copyTextToClipboard from 'ghost-admin/utils/copy-text-to-clipboard';
import {task, timeout} from 'ember-concurrency';
export default class ModalPostPreviewBrowserComponent extends Component {
@task
*copyPreviewUrl() {
copyTextToClipboard(this.args.post.previewUrl);
yield timeout(this.isTesting ? 50 : 3000);
}
}

View File

@ -0,0 +1,70 @@
<div class="gh-post-preview-container gh-post-preview-email-container {{unless @skipAnimation "fade-in"}}">
<div class="gh-post-preview-email-mockup">
<div class="gh-pe-emailclient-sender">
<p>
<span class="strong">{{or this.newsletter.senderName this.settings.title}}</span> &lt;{{full-email-address (or this.newsletter.senderEmail "noreply")}}&gt;
</p>
<p><span class="dark">To:</span> Jamie Larson &lt;jamie@example.com&gt;</p>
</div>
<iframe class="gh-pe-iframe" {{did-insert this.renderEmailPreview}} title="Email preview" sandbox="allow-same-origin allow-popups allow-popups-to-escape-sandbox"></iframe>
</div>
{{!-- <div class="flex">
<div class="gh-btn-group mr3">
<button type="button" class="gh-btn {{if (eq this.memberSegment "status:free") "gh-btn-group-selected"}}" {{on "click" (fn this.changeMemberSegment "status:free")}}><span>Free member</span></button>
<button type="button" class="gh-btn {{if (eq this.memberSegment "status:-free") "gh-btn-group-selected"}}" {{on "click" (fn this.changeMemberSegment "status:-free")}}><span>Paid member</span></button>
</div>
<div class="gh-post-preview-email-input {{if this.sendPreviewEmailError "error"}}">
<Input
@value={{this.previewEmailAddress}}
class="gh-input gh-post-preview-email-input"
placeholder="you@yoursite.com"
aria-invalid={{if this.sendPreviewEmailError "true"}}
aria-describedby={{if this.sendPreviewEmailError "sendError"}}
{{on-key "Enter" (perform this.sendPreviewEmailTask)}}
/>
{{#if this.sendPreviewEmailError}}
<div class="error fixed nowrap f8 lh-heading"><span class="response" id="sendError">{{this.sendPreviewEmailError}}</span></div>
{{/if}}
</div>
<GhTaskButton
@task={{this.sendPreviewEmailTask}}
@buttonText="Send test email"
@successText="Sent"
@runningText="Sending..."
@class="gh-btn gh-btn-green gh-btn-icon gh-post-preview-email-trigger"
/>
</div> --}}
</div>
<div class="gh-post-preview-email-footer">
<div class="gh-post-preview-email-test">
<div class="gh-btn-group mr3">
<button type="button" class="gh-btn {{if (eq this.memberSegment "status:free") "gh-btn-group-selected"}}" {{on "click" (fn this.changeMemberSegment "status:free")}}><span>Free member</span></button>
<button type="button" class="gh-btn {{if (eq this.memberSegment "status:-free") "gh-btn-group-selected"}}" {{on "click" (fn this.changeMemberSegment "status:-free")}}><span>Paid member</span></button>
</div>
<div class="gh-post-preview-email-input {{if this.sendPreviewEmailError "error"}}">
<Input
@value={{this.previewEmailAddress}}
class="gh-input gh-post-preview-email-input"
placeholder="you@yoursite.com"
aria-label="Email address to receive preview"
aria-invalid={{if this.sendPreviewEmailError "true"}}
aria-describedby={{if this.sendPreviewEmailError "sendError"}}
{{on-key "Enter" (perform this.sendPreviewEmailTask)}}
/>
{{#if this.sendPreviewEmailError}}
<div class="error fixed nowrap f8 lh-heading"><span class="response" id="sendError">{{this.sendPreviewEmailError}}</span></div>
{{/if}}
</div>
<GhTaskButton
@task={{this.sendPreviewEmailTask}}
@buttonText="Send test email"
@successText="Sent"
@runningText="Sending..."
@class="gh-btn gh-btn-icon gh-post-preview-email-trigger"
/>
</div>
</div>

View File

@ -0,0 +1,154 @@
import Component from '@glimmer/component';
import validator from 'validator';
import {action} from '@ember/object';
import {htmlSafe} from '@ember/template';
import {inject as service} from '@ember/service';
import {task, timeout} from 'ember-concurrency';
import {tracked} from '@glimmer/tracking';
const INJECTED_CSS = `
html::-webkit-scrollbar {
display: none;
width: 0;
background: transparent
}
html {
scrollbar-width: none;
}
`;
// TODO: remove duplication with <ModalPostEmailPreview>
export default class ModalPostPreviewEmailComponent extends Component {
@service ajax;
@service config;
@service feature;
@service ghostPaths;
@service session;
@service settings;
@service store;
@tracked html = '';
@tracked subject = '';
@tracked memberSegment = 'status:free';
@tracked previewEmailAddress = this.session.user.email;
@tracked sendPreviewEmailError = '';
get newsletter() {
return this.args.post.newsletter || this.args.newsletter;
}
get mailgunIsEnabled() {
return this.config.get('mailgunIsConfigured') ||
!!(this.settings.get('mailgunApiKey') && this.settings.get('mailgunDomain') && this.settings.get('mailgunBaseUrl'));
}
@action
async renderEmailPreview(iframe) {
this._previewIframe = iframe;
await this._fetchEmailData();
// avoid timing issues when _fetchEmailData didn't perform any async ops
await timeout(100);
if (iframe) {
iframe.contentWindow.document.open();
iframe.contentWindow.document.write(this.html);
iframe.contentWindow.document.close();
}
}
@action
changeMemberSegment(segment) {
this.memberSegment = segment;
if (this._previewIframe) {
this.renderEmailPreview(this._previewIframe);
}
}
@task({drop: true})
*sendPreviewEmailTask() {
try {
const resourceId = this.args.post.id;
const testEmail = this.previewEmailAddress.trim();
if (!validator.isEmail(testEmail)) {
this.sendPreviewEmailError = 'Please enter a valid email';
return false;
}
if (!this.mailgunIsEnabled) {
this.sendPreviewEmailError = 'Please verify your email settings';
return false;
}
this.sendPreviewEmailError = '';
const url = this.ghostPaths.url.api('/email_previews/posts', resourceId);
const data = {emails: [testEmail], memberSegment: this.memberSegment};
const options = {
data,
dataType: 'json'
};
yield this.ajax.post(url, options);
return true;
} catch (error) {
if (error) {
let message = 'Email could not be sent, verify mail settings';
// grab custom error message if present
if (
error.payload && error.payload.errors
&& error.payload.errors[0] && error.payload.errors[0].message) {
message = htmlSafe(error.payload.errors[0].message);
}
this.sendPreviewEmailError = message;
throw error;
}
}
}
async _fetchEmailData() {
let {html, subject, memberSegment} = this;
let {post} = this.args;
if (html && subject && memberSegment === this._lastMemberSegment) {
return {html, subject};
}
this._lastMemberSegment = memberSegment;
// model is an email
if (post.html && post.subject) {
html = post.html;
subject = post.subject;
// model is a post with an existing email
} else if (post.email) {
html = post.email.html;
subject = post.email.subject;
// model is a post, fetch email preview
} else {
let url = new URL(this.ghostPaths.url.api('/email_previews/posts', post.id), window.location.href);
url.searchParams.set('memberSegment', this.memberSegment);
url.searchParams.set('newsletter', this.newsletter.slug);
let response = await this.ajax.request(url.href);
let [emailPreview] = response.email_previews;
html = emailPreview.html;
subject = emailPreview.subject;
}
// inject extra CSS into the html for disabling links and scrollbars etc
let domParser = new DOMParser();
let htmlDoc = domParser.parseFromString(html, 'text/html');
let stylesheet = htmlDoc.querySelector('style');
let originalCss = stylesheet.innerHTML;
stylesheet.innerHTML = `${originalCss}\n\n${INJECTED_CSS}`;
const doctype = new XMLSerializer().serializeToString(htmlDoc.doctype);
html = doctype + htmlDoc.documentElement.outerHTML;
this.html = html;
this.subject = subject;
}
}

View File

@ -0,0 +1,7 @@
<div class="modal-body modal-preview-email-content gh-pe-mobile-container gh-post-preview-container h-auto overflow-auto {{unless @skipAnimation "fade-in"}}">
<div class="gh-pe-mobile-bezel">
<div class="gh-pe-mobile-screen">
<iframe class="gh-post-preview-iframe" src={{@post.previewUrl}} title="Mobile browser post preview"></iframe>
</div>
</div>
</div>

View File

@ -0,0 +1,11 @@
import Component from '@glimmer/component';
import copyTextToClipboard from 'ghost-admin/utils/copy-text-to-clipboard';
import {task, timeout} from 'ember-concurrency';
export default class ModalPostPreviewBrowserComponent extends Component {
@task
*copyPreviewUrl() {
copyTextToClipboard(this.args.post.previewUrl);
yield timeout(this.isTesting ? 50 : 3000);
}
}

View File

@ -0,0 +1,263 @@
<div class="gh-post-preview-container gh-post-preview-social-container {{unless @skipAnimation "fade-in"}}">
<p class="mb8 fw5">This is how your content will look when shared, you can click on any elements youd like to edit.</p>
<div class="flex flex-column">
<div class="flex gh-social-container-responsive">
<div class="gh-social-og-container">
<div class="flex ma3 mb2">
<span>{{svg-jar "social-facebook" class="social-icon"}}</span>
<div>
<div class="gh-social-og-title">{{or this.settings.metaTitle this.settings.title}}</div>
<div class="gh-social-og-time">12 hrs</div>
</div>
</div>
<div class="flex flex-column ma3 mt2">
<span class="gh-social-og-desc w-100 mb2" />
<span class="gh-social-og-desc w-100 mb2" />
<span class="gh-social-og-desc w-60" />
</div>
<div
class="gh-social-og-preview"
{{on "mouseenter" (fn (mut this.facebookHovered) true)}}
{{on "mouseleave" (fn (mut this.facebookHovered) false)}}
>
{{#if (and this.facebookHovered (not this.facebookImage))}}
{{!-- only shown on hover when there's no image or fallback --}}
<button class="gh-social-og-preview-img-add" type="button" {{on "click" (fn this.triggerFileDialog "facebook")}}>+ Add image</button>
{{/if}}
<GhUploader
@extensions={{this.imageExtensions}}
@onComplete={{this.setFacebookImage}}
as |uploader|
>
{{#each uploader.errors as |error|}}
<div class="error pa2"><span class="response">{{or error.context error.message}}</span></div>
{{/each}}
{{#if (or this.facebookImage uploader.isUploading)}}
<div class="gh-social-og-preview-image relative" style={{background-image-style this.facebookImage}}>
<div class="flex h-100 items-center justify-center">
{{#if (or this.facebookHovered uploader.isUploading)}}
{{#if uploader.isUploading}}
{{uploader.progressBar}}
{{else}}
<button type="button" class="gh-btn gh-btn-white" {{on "click" (fn this.triggerFileDialog "facebook")}}><span>{{if @post.ogImage "Change" "Upload"}} image</span></button>
{{/if}}
{{/if}}
{{#if (and this.facebookHovered @post.ogImage)}}
<button type="button" class="gh-btn gh-btn-black gh-btn-icon gh-social-preview-img-delete" title="Remove custom Facebook image" {{on "click" this.clearFacebookImage}}>
<span>{{svg-jar "trash"}}</span>
<span class="hidden">Remove custom Facebook image</span>
</button>
{{/if}}
</div>
</div>
{{/if}}
<div style="display:none">
<GhFileInput id="facebookFileInput" @multiple={{false}} @action={{uploader.setFiles}} @accept={{this.imageMimeTypes}} />
</div>
</GhUploader>
<div class="gh-social-og-preview-bookmark">
{{!-- Ensures description is hidden if title exceeds one line --}}
<div class="gh-social-og-preview-content {{if this.editingFacebookTitle 'edit-mode'}} {{if this.editingFacebookDescription 'edit-mode'}}">
<div class="gh-social-og-preview-meta">
{{this.config.blogDomain}}
</div>
{{#if this.editingFacebookTitle}}
<input
aria-label="Facebook title"
type="text"
class="gh-input"
placeholder={{this.facebookTitle}}
value={{@post.ogTitle}}
maxlength="300"
{{on "blur" this.setFacebookTitle}}
{{on-key "Enter" this.blurElement}}
{{on-key "Escape" (fn this.cancelEdit "ogTitle")}}
{{autofocus}}
data-prevent-escape-close-modal="true"
/>
{{else}}
<div class="gh-social-og-preview-title editable pointer" role="button" aria-label="Edit Facebook title" {{on "click" this.editFacebookTitle}}>
{{truncate this.facebookTitle}}
</div>
{{/if}}
{{#if this.editingFacebookDescription}}
<textarea
aria-label="Facebook description"
class="gh-input"
maxlength="500"
placeholder={{truncate this.facebookDescription 160}}
{{on "blur" this.setFacebookDescription}}
{{on-key "Enter" this.blurElement}}
{{on-key "Escape" (fn this.cancelEdit "ogDescription")}}
{{autofocus}}
data-prevent-escape-close-modal="true"
>{{@post.ogDescription}}</textarea>
{{else}}
<div class="gh-social-og-preview-desc editable pointer" role="button" aria-label="Edit Facebook description" {{on "click" this.editFacebookDescription}}>
{{truncate this.facebookDescription}}
</div>
{{/if}}
</div>
</div>
</div>
<div class="gh-social-og-reactions">
<span class="gh-social-og-likes">{{svg-jar "facebook-like" class="z-999"}}{{svg-jar "facebook-heart" class="nl1"}}182</span>
<span class="gh-social-og-comments">7 comments</span>
<span class="gh-social-og-comments ml2">2 shares</span>
</div>
</div>
<div class="gh-social-twitter-container">
<div class="flex ma4">
<span>{{svg-jar "social-twitter" class="social-icon"}}</span>
<div>
<span class="gh-social-og-title">{{or this.settings.metaTitle this.settings.title}}</span>
<span class="gh-social-og-time">12 hrs</span>
<div class="flex flex-column mt2 mb3">
<span class="gh-social-og-desc w-100 mb2" />
<span class="gh-social-og-desc w-60" />
</div>
<div class="gh-social-twitter-post-preview"
{{on "mouseenter" (fn (mut this.twitterHovered) true)}}
{{on "mouseleave" (fn (mut this.twitterHovered) false)}}
>
{{#if (and this.twitterHovered (not this.twitterImage))}}
{{!-- only shown on hover when there's no image or fallback --}}
<button class="gh-social-twitter-preview-img-add" type="button" {{on "click" (fn this.triggerFileDialog "twitter")}}>+ Add image</button>
{{/if}}
<GhUploader
@extensions={{this.imageExtensions}}
@onComplete={{this.setTwitterImage}}
as |uploader|
>
{{#each uploader.errors as |error|}}
<div class="error pa2"><span class="response">{{or error.context error.message}}</span></div>
{{/each}}
{{#if (or this.twitterImage uploader.isUploading)}}
<div class="gh-social-twitter-preview-image relative" style={{background-image-style this.twitterImage}}>
<div class="flex h-100 items-center justify-center">
{{#if (or this.twitterHovered uploader.isUploading)}}
{{#if uploader.isUploading}}
{{uploader.progressBar}}
{{else}}
<button type="button" class="gh-btn gh-btn-white" {{on "click" (fn this.triggerFileDialog "twitter")}}><span>{{if @post.twitterImage "Change" "Upload"}} image</span></button>
{{/if}}
{{/if}}
{{#if (and this.twitterHovered @post.twitterImage)}}
<button type="button" class="gh-btn gh-btn-black gh-btn-icon gh-social-preview-img-delete" title="Remove custom Twitter image" {{on "click" this.clearTwitterImage}}>
<span>{{svg-jar "trash"}}</span>
<span class="hidden">Remove custom Twitter image</span>
</button>
{{/if}}
</div>
</div>
{{/if}}
<div style="display:none">
<GhFileInput id="twitterFileInput" @multiple={{false}} @action={{uploader.setFiles}} @accept={{this.imageMimeTypes}} />
</div>
</GhUploader>
<div class="gh-social-twitter-preview-content">
{{#if this.editingTwitterTitle}}
<input
aria-label="Twitter title"
type="text"
class="gh-input"
placeholder={{this.twitterTitle}}
value={{@post.twitterTitle}}
maxlength="300"
{{on "blur" this.setTwitterTitle}}
{{on-key "Enter" this.blurElement}}
{{on-key "Escape" (fn this.cancelEdit "twitterTitle")}}
{{autofocus}}
data-prevent-escape-close-modal="true"
/>
{{else}}
<div class="gh-social-twitter-preview-title editable pointer" role="button" aria-label="Edit Twitter title" {{on "click" this.editTwitterTitle}}>{{this.twitterTitle}}</div>
{{/if}}
{{#if this.editingTwitterDescription}}
<textarea
aria-label="Twitter description"
class="gh-input"
maxlength="500"
placeholder={{truncate this.twitterDescription 160}}
{{on "blur" this.setTwitterDescription}}
{{on-key "Enter" this.blurElement}}
{{on-key "Escape" (fn this.cancelEdit "twitterDescription")}}
{{autofocus}}
data-prevent-escape-close-modal="true"
>{{@post.twitterDescription}}</textarea>
{{else}}
<div class="gh-social-twitter-preview-desc editable pointer" role="button" aria-label="Edit Twitter description" {{on "click" this.editTwitterDescription}}>{{truncate this.twitterDescription}}</div>
{{/if}}
<div class="gh-social-twitter-preview-meta">
{{svg-jar "twitter-link"}}
{{this.config.blogDomain}}
</div>
</div>
</div>
<div class="gh-social-twitter-reactions">
<div class="flex items-center">{{svg-jar "twitter-comment"}}2</div>
<div class="flex items-center">{{svg-jar "twitter-retweet"}}11</div>
<div class="flex items-center">{{svg-jar "twitter-like"}}32</div>
<div class="flex items-center">{{svg-jar "twitter-share"}}</div>
</div>
</div>
</div>
</div>
</div>
<div class="gh-seo-preview-container">
{{svg-jar "google"}}
<div class="gh-seo-preview">
<div class="gh-seo-search-bar mb12">{{svg-jar "google-search"}}</div>
<div class="gh-seo-preview-link">{{this.serpURL}}</div>
{{#if this.editingMetaTitle}}
<input
aria-label="Meta title"
type="text"
class="gh-input"
placeholder={{this.serpTitle}}
value={{@post.metaTitle}}
maxlength="300"
{{on "blur" this.setMetaTitle}}
{{on-key "Enter" this.blurElement}}
{{on-key "Escape" (fn this.cancelEdit "metaTitle")}}
{{autofocus}}
data-prevent-escape-close-modal="true"
>
{{else}}
<div class="gh-seo-preview-title editable pointer" role="button" aria-label="Edit meta title" {{on "click" this.editMetaTitle}}>
{{this.serpTitle}}
</div>
{{/if}}
{{#if this.editingMetaDescription}}
<textarea
aria-label="Meta description"
class="gh-input"
placeholder={{this.serpDescription}}
maxlength="500"
{{on "blur" this.setMetaDescription}}
{{on-key "Enter" this.blurElement}}
{{on-key "Escape" (fn this.cancelEdit "metaDescription")}}
{{autofocus}}
data-prevent-escape-close-modal="true"
>{{@post.metaDescription}}</textarea>
{{else}}
<div class="gh-seo-preview-desc editable pointer" role="button" aria-label="Edit meta description" {{on "click" this.editMetaDescription}}>
{{moment-format (now) "DD MMM YYYY"}}{{truncate this.serpDescription 149}}
</div>
{{/if}}
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,209 @@
import Component from '@glimmer/component';
import {
IMAGE_EXTENSIONS,
IMAGE_MIME_TYPES
} from 'ghost-admin/components/gh-image-uploader';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
import {tracked} from '@glimmer/tracking';
export default class ModalPostPreviewSocialComponent extends Component {
@service config;
@service settings;
@service ghostPaths;
@tracked editingFacebookTitle = false;
@tracked editingFacebookDescription = false;
@tracked editingTwitterTitle = false;
@tracked editingTwitterDescription = false;
@tracked editingMetaTitle = false;
@tracked editingMetaDescription = false;
imageExtensions = IMAGE_EXTENSIONS;
imageMimeTypes = IMAGE_MIME_TYPES;
get _fallbackDescription() {
return this.args.post.customExcerpt ||
this.serpDescription ||
this.settings.get('description');
}
@action
blurElement(event) {
if (!event.shiftKey) {
event.preventDefault();
event.target.blur();
}
}
@action
triggerFileDialog(name) {
const input = document.querySelector(`#${name}FileInput input`);
if (input) {
input.click();
}
}
// SERP
get serpTitle() {
return this.args.post.metaTitle || this.args.post.title || '(Untitled)';
}
get serpURL() {
const urlParts = [];
if (this.args.post.canonicalUrl) {
const canonicalUrl = new URL(this.args.post.canonicalUrl);
urlParts.push(canonicalUrl.host);
urlParts.push(...canonicalUrl.pathname.split('/').reject(p => !p));
} else {
const blogUrl = new URL(this.config.get('blogUrl'));
urlParts.push(blogUrl.host);
urlParts.push(...blogUrl.pathname.split('/').reject(p => !p));
urlParts.push(this.args.post.slug);
}
return urlParts.join(' > ');
}
get serpDescription() {
return this.args.post.metaDescription || this.args.post.excerpt;
}
@action
editMetaTitle() {
this.editingMetaTitle = true;
}
@action
setMetaTitle(event) {
const title = event.target.value;
this.args.post.metaTitle = title.trim();
this.args.post.save();
this.editingMetaTitle = false;
}
@action
editMetaDescription() {
this.editingMetaDescription = true;
}
@action
setMetaDescription(event) {
const description = event.target.value;
this.args.post.metaDescription = description.trim();
this.args.post.save();
this.editingMetaDescription = false;
}
// Facebook
get facebookTitle() {
return this.args.post.ogTitle || this.serpTitle;
}
get facebookDescription() {
return this.args.post.ogDescription || this._fallbackDescription;
}
get facebookImage() {
return this.args.post.ogImage || this.args.post.featureImage || this.settings.get('ogImage') || this.settings.get('coverImage');
}
@action
editFacebookTitle() {
this.editingFacebookTitle = true;
}
@action
cancelEdit(property, event) {
event.preventDefault();
event.target.value = this.args.post[property];
event.target.blur();
}
@action
setFacebookTitle(event) {
const title = event.target.value;
this.args.post.ogTitle = title.trim();
this.args.post.save();
this.editingFacebookTitle = false;
}
@action
editFacebookDescription() {
this.editingFacebookDescription = true;
}
@action
setFacebookDescription() {
const description = event.target.value;
this.args.post.ogDescription = description.trim();
this.args.post.save();
this.editingFacebookDescription = false;
}
@action
setFacebookImage([image]) {
this.args.post.ogImage = image.url;
this.args.post.save();
}
@action
clearFacebookImage() {
this.args.post.ogImage = null;
this.args.post.save();
}
// Twitter
get twitterTitle() {
return this.args.post.twitterTitle || this.serpTitle;
}
get twitterDescription() {
return this.args.post.twitterDescription || this._fallbackDescription;
}
get twitterImage() {
return this.args.post.twitterImage || this.args.post.featureImage || this.settings.get('twitterImage') || this.settings.get('coverImage');
}
@action
editTwitterTitle() {
this.editingTwitterTitle = true;
}
@action
setTwitterTitle(event) {
const title = event.target.value;
this.args.post.twitterTitle = title.trim();
this.args.post.save();
this.editingTwitterTitle = false;
}
@action
editTwitterDescription() {
this.editingTwitterDescription = true;
}
@action
setTwitterDescription() {
const description = event.target.value;
this.args.post.twitterDescription = description.trim();
this.args.post.save();
this.editingTwitterDescription = false;
}
@action
setTwitterImage([image]) {
this.args.post.twitterImage = image.url;
this.args.post.save();
}
@action
clearTwitterImage() {
this.args.post.twitterImage = null;
this.args.post.save();
}
}

View File

@ -0,0 +1,62 @@
<div class="flex flex-column h-100 items-center overflow-auto" data-test-modal="publish-flow">
<header class="gh-publish-header">
<button class="gh-btn-editor gh-publish-back-button" title="Close" type="button" {{on "click" @close}} data-test-button="close-publish-flow">
<span>{{svg-jar "arrow-left"}} Editor</span>
</button>
{{#if (and (not this.emailErrorMessage) (not this.isComplete))}}
<div class="flex items-center pe-auto h-100">
<button
type="button"
class="gh-btn gh-btn-editor gh-editor-preview-trigger"
{{on "click" @data.togglePreviewPublish}}
data-test-button="publish-flow-preview"
>
<span>Preview</span>
</button>
<button
type="button"
class="gh-btn gh-btn-editor darkgrey gh-publish-trigger active"
title="Close"
{{on "click" @close}}
data-test-button="publish-flow-publish"
>
<span>Publish</span>
</button>
<div class="settings-menu-toggle-spacer"></div>
</div>
{{/if}}
</header>
<div class="gh-publish-settings-container {{unless @data.skipAnimation "fade-in"}}">
{{#if this.emailErrorMessage}}
<Editor::Modals::PublishFlow::CompleteWithEmailError
@emailErrorMessage={{this.emailErrorMessage}}
@publishOptions={{@data.publishOptions}}
@close={{@close}}
/>
{{else if this.isConfirming}}
<Editor::Modals::PublishFlow::Confirm
@publishOptions={{@data.publishOptions}}
@recipientType={{this.recipientType}}
@saveTask={{this.saveTask}}
@cancel={{this.toggleConfirm}}
@close={{@close}}
/>
{{else if this.isComplete}}
<Editor::Modals::PublishFlow::Complete
@publishOptions={{@data.publishOptions}}
@recipientType={{this.recipientType}}
@postCount={{this.postCount}}
@close={{@close}}
/>
{{else}}
<Editor::Modals::PublishFlow::Options
@publishOptions={{@data.publishOptions}}
@recipientType={{this.recipientType}}
@confirm={{this.toggleConfirm}}
@close={{@close}}
/>
{{/if}}
</div>
</div>

View File

@ -0,0 +1,87 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency';
import {tracked} from '@glimmer/tracking';
export default class PublishModalComponent extends Component {
static modalOptions = {
className: 'fullscreen-modal-total-overlay publish-modal',
omitBackdrop: true,
ignoreBackdropClick: true
};
@service store;
@tracked emailErrorMessage;
@tracked isConfirming = false;
@tracked isComplete = false;
@tracked postCount = null;
get recipientType() {
const filter = this.args.data.publishOptions.recipientFilter;
if (!filter) {
return 'none';
}
if (filter === 'status:free') {
return 'free';
}
if (filter === 'status:-free') {
return 'paid';
}
if (filter.includes('status:free') && filter.includes('status:-free')) {
return 'all';
}
return 'specific';
}
@action
toggleConfirm() {
this.isConfirming = !this.isConfirming;
if (this.isConfirming) {
this.fetchPostCountTask.perform();
}
}
@task
*saveTask() {
try {
yield this.args.data.saveTask.perform();
this.isConfirming = false;
this.isComplete = true;
} catch (e) {
if (e?.name === 'EmailFailedError') {
return this.emailErrorMessage = e.message;
}
throw e;
}
}
// we fetch the new post count in advance when reaching the confirm step
// to avoid a copy flash when reaching the complete step
@task
*fetchPostCountTask() {
const publishOptions = this.args.data.publishOptions;
// no count is shown for pages, scheduled posts, or email-only posts
if (publishOptions.post.isPage || publishOptions.isScheduled || !publishOptions.willPublish) {
this.postCount = null;
return;
}
const result = yield this.store.query('post', {filter: 'status:published', limit: 1});
let count = result.meta.pagination.total;
count += 1; // account for the new post
this.postCount = count;
}
}

View File

@ -0,0 +1,35 @@
{{#let @publishOptions.post as |post|}}
<div class="gh-publish-title">
<span class="red">Uh-oh.</span>
{{#if @publishOptions.willOnlyEmail}}
Your post has been created but the email failed to send.
{{else}}
Your {{post.displayName}} has been published but the email failed to send.
{{/if}}
</div>
<p class="gh-publish-confirmation">
{{this.emailErrorMessage}}
<br><br>
Please verify your email settings if the error persists.
</p>
{{#if this.retryErrorMessage}}
<p class="error gh-box gh-box-error mt3 mb3">
{{this.retryErrorMessage}}
</p>
{{/if}}
<div class="gh-publish-cta">
<GhTaskButton
@task={{this.retryEmailTask}}
@buttonText="Retry sending email"
@runningText="Sending"
@successText="Sent"
@class="gh-btn gh-btn-large gh-btn-red"
@showIcon={{false}}
/>
</div>
{{/let}}

View File

@ -0,0 +1,71 @@
import Component from '@glimmer/component';
import EmailFailedError from 'ghost-admin/errors/email-failed-error';
import {CONFIRM_EMAIL_MAX_POLL_LENGTH, CONFIRM_EMAIL_POLL_LENGTH} from '../../publish-management';
import {htmlSafe} from '@ember/template';
import {isServerUnreachableError} from 'ghost-admin/services/ajax';
import {task, timeout} from 'ember-concurrency';
import {tracked} from '@glimmer/tracking';
function isString(str) {
return toString.call(str) === '[object String]';
}
export default class PublishFlowCompleteWithEmailError extends Component {
@tracked newEmailErrorMessage;
@tracked retryErrorMessage;
get emailErrorMessage() {
return this.newEmailErrorMessage || this.args.emailErrorMessage;
}
@task({drop: true})
*retryEmailTask() {
this.retryErrorMessage = null;
try {
let email = yield this.args.publishOptions.post.email.retry();
let pollTimeout = 0;
if (email && email.status !== 'submitted') {
while (pollTimeout < CONFIRM_EMAIL_MAX_POLL_LENGTH) {
yield timeout(CONFIRM_EMAIL_POLL_LENGTH);
pollTimeout += CONFIRM_EMAIL_POLL_LENGTH;
email = yield email.reload();
if (email.status === 'submitted') {
break;
}
if (email.status === 'failed') {
throw new EmailFailedError(email.error);
}
}
}
return email;
} catch (e) {
// update "failed" state if email fails again
if (e && e.name === 'EmailFailedError') {
this.newEmailErrorMessage = e.message;
return false;
}
if (e) {
let errorMessage = '';
if (isServerUnreachableError(e)) {
errorMessage = 'Unable to connect, please check your internet connection and try again.';
} else if (e && isString(e)) {
errorMessage = e;
} else if (e?.payload?.errors?.[0].message) {
errorMessage = e.payload.errors[0].message;
} else {
errorMessage = 'Unknown Error occurred when attempting to resend';
}
this.retryErrorMessage = htmlSafe(errorMessage);
return false;
}
}
}
}

View File

@ -0,0 +1,112 @@
{{#let @publishOptions.post as |post|}}
<div class="gh-publish-title" data-test-publish-flow="complete" data-test-complete-title>
{{#if post.isScheduled}}
<span class="green">All set!</span>
Your {{if post.emailOnly "email" post.displayName}} will be
{{#if post.emailOnly}}
sent
{{else if post.willEmail}}
published and sent
{{else}}
published
{{/if}}
{{#let (moment-site-tz post.publishedAtUTC) as |scheduledAt|}}
{{#if (is-moment-today scheduledAt)}}
today
{{else}}
on {{moment-format scheduledAt "MMMM Do"}}
{{/if}}
at {{moment-format scheduledAt "HH:mm"}}.
{{/let}}
{{else}}
<span class="green">Boom. Its out there.</span>
{{#if post.emailOnly}}
Your email has been sent.
{{else if (and post.isPost @postCount)}}
Thats {{gh-pluralize @postCount "post"}} published, keep going!
{{else}}
Your {{post.displayName}} has been published.
{{/if}}
{{/if}}
</div>
{{#if post.emailOnly}}
<div class="gh-publish-confirmation" data-test-complete-details>
<p>
Your post
{{if post.isScheduled "will be" "was"}}
sent to
{{#let (members-count-fetcher query=(hash filter=post.fullRecipientFilter)) as |countFetcher|}}
<strong>
{{if (eq @recipientType "all") "all"}}
{{countFetcher.count}}
{{!-- @recipientType = free/paid/all/specific --}}
{{if (not-eq @recipientType "all") @recipientType}}
{{gh-pluralize countFetcher.count "subscriber" without-count=true}}
</strong>
{{#unless @publishOptions.onlyDefaultNewsletter}}
of <strong>{{@publishOptions.newsletter.name}}</strong>{{if post.isScheduled "," "."}}
{{/unless}}
{{/let}}
{{#let (moment-site-tz post.publishedAtUTC) as |publishedAt|}}
on
{{moment-format publishedAt "D MMM YYYY"}}
at
{{moment-format publishedAt "HH:mm"}}.
{{/let}}
</p>
{{#if post.isScheduled}}
<p>
Need to make a change?
<button
type="button"
class="gh-revert-to-draft"
{{on "click" (fn @close (hash afterTask="revertToDraftTask"))}}
>
<span>Unschedule and revert to draft &rarr;</span>
</button>
</p>
{{/if}}
</div>
{{else}}
<a href={{post.url}} class="gh-post-bookmark-wrapper" target="_blank" rel="noopener noreferrer" data-test-complete-bookmark>
<GhPostBookmark @post={{post}} />
</a>
{{#if post.isScheduled}}
<p class="gh-publish-confirmation">
Need to make a change?
<button
type="button"
class="gh-revert-to-draft"
{{on "click" (fn @close (hash afterTask="revertToDraftTask"))}}
data-test-button="revert-to-draft"
>
<span>Unschedule and revert to draft &rarr;</span>
</button>
</p>
{{else}}
<p class="gh-publish-confirmation">
<button
type="button"
class="gh-back-to-editor"
{{on "click" @close}}
data-test-button="back-to-editor"
>
<span>Back to editor</span>
</button>
</p>
{{/if}}
{{/if}}
{{/let}}

View File

@ -0,0 +1,73 @@
<div class="gh-publish-title" data-test-publish-flow="confirm">
<div class="green">Ready, set, publish.</div>
<div>Share it with the world.</div>
</div>
<p class="gh-publish-confirmation" data-test-text="confirm-details">
{{#if @publishOptions.isScheduled}}
{{#let (moment-site-tz @publishOptions.scheduledAtUTC) as |scheduledAt|}}
On
<strong>
{{moment-format scheduledAt "D MMM YYYY"}}
at
{{moment-format scheduledAt "HH:mm"}}
</strong>
your
{{/let}}
{{else}}
Your
{{/if}}
{{@publishOptions.post.displayName}}
{{#if this.willPublish}}
will be published on your site{{#if this.willEmail}}, and delivered to{{else}}.{{/if}}
{{/if}}
{{#if this.willEmail}}
{{#unless this.willPublish}}
will be delivered to
{{/unless}}
{{#let (members-count-fetcher query=(hash filter=@publishOptions.fullRecipientFilter)) as |countFetcher|}}
<strong>
{{if (eq @recipientType "all") "all"}}
{{format-number countFetcher.count}}
{{!-- @recipientType = none/free/paid/all/specific --}}
{{if (not-eq @recipientType "all") @recipientType}}
</strong>
{{#if @publishOptions.onlyDefaultNewsletter}}
<strong>{{gh-pluralize countFetcher.count "subscriber" without-count=true}}</strong>{{#if this.willPublish}}.{{else}},{{/if}}
{{else}}
<strong>{{gh-pluralize countFetcher.count "subscriber" without-count=true}}</strong>
of <strong>{{@publishOptions.newsletter.name}}</strong>{{#if this.willPublish}}.{{else}},{{/if}}
{{/if}}
{{/let}}
{{#unless this.willPublish}}
and will <strong>not</strong> be published on your site.
{{/unless}}
{{/if}}
</p>
{{#if this.errorMessage}}
<p class="error gh-box gh-box-error mt3 mb3" data-test-confirm-error>
{{this.errorMessage}}
</p>
{{/if}}
<div class="gh-publish-cta">
<GhTaskButton
@task={{this.confirmTask}}
@buttonText={{this.confirmButtonText}}
@runningText={{this.confirmRunningText}}
@successText={{this.confirmSuccessText}}
@class="gh-btn gh-btn-large"
@idleClass="gh-btn-pulse"
@runningClass="gh-btn-green gh-btn-icon"
data-test-button="confirm-publish"
/>
<button type="button" class="gh-btn gh-btn-link gh-btn-large gh-publish-cta-secondary" {{on "click" @cancel}} data-test-button="back-to-options">Back to settings</button>
</div>

View File

@ -0,0 +1,122 @@
import Component from '@glimmer/component';
import moment from 'moment';
import {htmlSafe} from '@ember/template';
import {isArray} from '@ember/array';
import {isServerUnreachableError} from 'ghost-admin/services/ajax';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency';
import {tracked} from '@glimmer/tracking';
function isString(str) {
return toString.call(str) === '[object String]';
}
export default class PublishFlowOptions extends Component {
@service settings;
@tracked errorMessage;
// store any derived state from PublishOptions on creation so the copy
// doesn't change whilst the post is saving
willEmail = this.args.publishOptions.willEmail;
willPublish = this.args.publishOptions.willPublish;
buttonTextMap = {
'publish+send': {
idle: 'Publish & send',
running: 'Publishing & sending',
success: 'Published & sent'
},
send: {
idle: 'Send email',
running: 'Sending',
success: 'Sent'
},
publish: {
idle: 'Publish',
running: 'Publishing',
success: 'Published'
},
schedule: {
// idle: '', - uses underlying publish type text
running: 'Scheduling',
success: 'Scheduled'
}
};
get publishType() {
const {publishOptions} = this.args;
if (this.willPublish && this.willEmail) {
return 'publish+send';
} else if (publishOptions.willOnlyEmail) {
return 'send';
} else {
return 'publish';
}
}
get confirmButtonText() {
let buttonText = '';
buttonText = this.buttonTextMap[this.publishType].idle;
if (this.publishType === 'publish') {
buttonText += ` ${this.args.publishOptions.post.displayName}`;
}
if (this.args.publishOptions.isScheduled) {
const scheduleMoment = moment.tz(this.args.publishOptions.scheduledAtUTC, this.settings.get('timezone'));
buttonText += `, on ${scheduleMoment.format('MMMM Do')}`;
} else {
buttonText += ', right now';
}
return buttonText;
}
get confirmRunningText() {
const publishType = this.args.publishOptions.isScheduled ? 'schedule' : this.publishType;
return this.buttonTextMap[publishType].running;
}
get confirmSuccessText() {
const publishType = this.args.publishOptions.isScheduled ? 'schedule' : this.publishType;
return this.buttonTextMap[publishType].success;
}
@task({drop: true})
*confirmTask() {
this.errorMessage = null;
try {
yield this.args.saveTask.perform();
} catch (e) {
if (e === undefined && this.args.publishOptions.post.errors.length !== 0) {
// validation error
const validationError = this.args.publishOptions.post.errors.messages[0];
this.errorMessage = `Validation failed: ${validationError}`;
return false;
}
let errorMessage = '';
if (isServerUnreachableError(e)) {
errorMessage = 'Unable to connect, please check your internet connection and try again.';
} else if (e && isString(e)) {
errorMessage = e;
} else if (e && isArray(e)) {
// This is here because validation errors are returned as an array
// TODO: remove this once validations are fixed
errorMessage = e[0];
} else if (e?.payload?.errors?.[0].message) {
errorMessage = e.payload.errors[0].message;
} else {
errorMessage = 'Unknown Error';
}
this.errorMessage = htmlSafe(errorMessage);
return false;
}
}
}

View File

@ -0,0 +1,143 @@
<div class="gh-publish-title" data-test-publish-flow="options">
<div class="green">Ready, set, publish.</div>
<div>Share it with the world.</div>
</div>
<div class="gh-publish-settings">
<div class="gh-publish-setting" data-test-setting="publish-type">
{{#if @publishOptions.emailUnavailable}}
<div class="gh-publish-setting-title" data-test-setting-title>
{{svg-jar "send-email"}}
<div class="gh-publish-setting-trigger">Publish on site</div>
</div>
{{else}}
<button type="button" class="gh-publish-setting-title" {{on "click" (fn this.toggleSection "publishType")}} data-test-setting-title>
{{svg-jar "send-email"}}
<div class="gh-publish-setting-trigger">
<span>{{@publishOptions.selectedPublishTypeOption.display}}</span>
</div>
<span class="{{if (eq this.openSection "publishType") "expanded"}}">
{{svg-jar "arrow-down" class="icon-expand"}}
</span>
</button>
{{/if}}
{{#liquid-if (eq this.openSection "publishType")}}
<div class="gh-publish-setting-form">
<Editor::PublishOptions::PublishType
@publishOptions={{@publishOptions}}
/>
</div>
{{/liquid-if}}
</div>
{{#unless @publishOptions.emailUnavailable}}
<div class="gh-publish-setting" data-test-setting="email-recipients">
{{#if (not-eq @publishOptions.publishType "publish")}}
<button
type="button"
class="gh-publish-setting-title"
{{on "click" (fn this.toggleSection "emailRecipients")}}
data-test-setting-title
>
{{svg-jar "member"}}
<div class="gh-publish-setting-trigger">
{{#if @publishOptions.recipientFilter}}
{{#let (members-count-fetcher query=(hash filter=@publishOptions.fullRecipientFilter)) as |countFetcher|}}
{{#if (or (gt countFetcher.count 1) (is-empty countFetcher.count))}}
{{if (eq @recipientType "all") "All"}}
{{/if}}
{{format-number countFetcher.count}}
{{!-- @recipientType = all/free/paid/all/specific --}}
{{#if (not-eq @recipientType "all")}}
{{if (is-empty countFetcher.count) (capitalize @recipientType) @recipientType}}
{{/if}}
{{gh-pluralize countFetcher.count "subscriber" without-count=true}}
{{#unless @publishOptions.onlyDefaultNewsletter}}
of <span class="gh-selected-newsletter">{{@publishOptions.newsletter.name}}</span>
{{/unless}}
{{/let}}
{{else}}
Not sent as newsletter
{{/if}}
</div>
<span class="{{if (eq this.openSection "emailRecipients") "expanded"}}">
{{svg-jar "arrow-down" class="icon-expand"}}
</span>
</button>
{{else}}
<button
type="button"
class="gh-publish-setting-title disabled"
data-test-setting-title
>
{{svg-jar "member"}}
<div class="gh-publish-setting-trigger">
Not sent as newsletter
</div>
<span>
{{svg-jar "arrow-down" class="icon-expand"}}
</span>
</button>
{{/if}}
{{#liquid-if (eq this.openSection "emailRecipients")}}
<div class="gh-publish-setting-form">
<Editor::PublishOptions::EmailRecipients
@publishOptions={{@publishOptions}}
/>
</div>
{{/liquid-if}}
</div>
{{/unless}}
{{#if (and @publishOptions.post.email (not @publishOptions.emailDisabledInSettings))}}
<div class="gh-publish-setting" data-test-setting="email-recipients">
<div class="gh-publish-setting-title disabled" data-test-setting-title>
{{svg-jar "member"}}
<div class="gh-publish-setting-trigger">
Already sent to
{{@publishOptions.post.email.emailCount}}
{{if (not-eq @recipientType "all") @recipientType}} {{!-- free/paid/specific --}}
{{gh-pluralize @publishOptions.post.email.emailCount "subscriber" without-count=true}}
{{#unless @publishOptions.onlyDefaultNewsletter}}
of {{@publishOptions.post.newsletter.name}}
{{/unless}}
</div>
</div>
</div>
{{/if}}
<div class="gh-publish-setting last" data-test-setting="publish-at">
<button type="button" class="gh-publish-setting-title" {{on "click" (fn this.toggleSection "publishAt")}} data-test-setting-title>
{{svg-jar "clock"}}
<div class="gh-publish-setting-trigger">
<span>
{{#if @publishOptions.isScheduled}}
{{capitalize (gh-format-post-time @publishOptions.scheduledAtUTC draft=true)}}
{{else}}
Right now
{{/if}}
</span>
</div>
<span class="{{if (eq this.openSection "publishAt") "expanded"}}">
{{svg-jar "arrow-down" class="icon-expand"}}
</span>
</button>
{{#liquid-if (eq this.openSection "publishAt")}}
<div class="gh-publish-setting-form last">
<Editor::PublishOptions::PublishAt
@publishOptions={{@publishOptions}}
/>
</div>
{{/liquid-if}}
</div>
</div>
<div class="gh-publish-cta">
<button type="button" class="gh-btn gh-btn-black gh-btn-large" {{on "click" @confirm}} data-test-button="continue">
<span>Continue, final review &rarr;</span>
</button>
</div>

View File

@ -0,0 +1,16 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';
import {tracked} from '@glimmer/tracking';
export default class PublishFlowOptions extends Component {
@tracked openSection = null;
@action
toggleSection(section) {
if (section === this.openSection) {
this.openSection = null;
} else {
this.openSection = section;
}
}
}

View File

@ -0,0 +1,112 @@
<div class="flex flex-column h-100 items-center overflow-auto" data-test-modal="update-flow">
<header class="gh-publish-header">
<button class="gh-btn-editor gh-publish-back-button" title="Close" type="button" {{on "click" @close}}>
<span>{{svg-jar "arrow-left"}} Editor</span>
</button>
<div class="flex">
{{#let @data.publishOptions.post as |post|}}
{{#if (not (and post.isSent (not post.isPublished)))}}
<button class="gh-btn gh-btn-editor darkgrey gh-publish-trigger active" title="Close" type="button" {{on "click" @close}}>
<span>{{if post.isScheduled "Unschedule" "Unpublish"}}</span>
</button>
{{/if}}
{{/let}}
<div class="settings-menu-toggle-spacer"></div>
</div>
</header>
{{#let @data.publishOptions.post as |post|}}
<div class="gh-publish-settings-container gh-update-flow fade-in">
<div class="gh-publish-title" data-test-update-flow-title>
{{#if (and post.isSent (not post.isPublished))}}
This {{post.displayName}} was
<span class="green">{{post.status}} by email</span>
{{else}}
This {{post.displayName}} has been
<span class="green">{{post.status}}</span>
{{/if}}
</div>
<div class="gh-publish-confirmation" data-test-update-flow-confirmation>
<p>
Your
{{post.displayName}}
{{if post.isScheduled "will be" "was"}}
{{#if
(or post.isSent
(and post.isPublished post.email)
post.willEmail
)
}}
{{#if post.emailOnly}}
sent to
{{else}}
published and sent to
{{/if}}
{{#if post.isScheduled}}
{{#let (members-count-fetcher query=(hash filter=post.fullRecipientFilter)) as |countFetcher|}}
<strong>{{gh-pluralize countFetcher.count "subscriber"}}</strong>
{{#if this.showNewsletterName}}of <strong>{{post.newsletter.name}}</strong>{{/if}}
{{/let}}
{{else}}
<strong>{{gh-pluralize post.email.emailCount "subscriber"}}</strong>
{{#if this.showNewsletterName}}of <strong>{{post.newsletter.name}}</strong>{{/if}}
{{/if}}
{{else}}
published on your site
{{/if}}
{{#let (moment-site-tz post.publishedAtUTC) as |publishedAt|}}
on
{{moment-format publishedAt "D MMM YYYY"}}
at
{{moment-format publishedAt "HH:mm"}}.
{{/let}}
{{#if post.isScheduled}}
{{#if (and post.isScheduled post.email)}}
This post was previously emailed to
<strong>{{pluralize post.email.emailCount "subscriber"}}</strong>
{{#if this.showNewsletterName}}
of <strong>{{post.newsletter.name}}</strong>
{{/if}}
{{#let (moment-site-tz post.email.createdAtUTC) as |sentAt|}}
on
{{moment-format sentAt "D MMM YYYY"}}
at
{{moment-format sentAt "HH:mm"}}.
{{/let}}
{{/if}}
<br><br>
Need to make a change?
<button
type="button"
class="gh-revert-to-draft"
{{on "click" (fn @close (hash afterTask="revertToDraftTask"))}}
data-test-button="revert-to-draft"
>
<span>Unschedule and revert to draft &rarr;</span>
</button>
{{else if (not post.emailOnly)}}
<br><br>
<button
type="button"
class="gh-revert-to-draft"
{{on "click" (fn @close (hash afterTask="revertToDraftTask"))}}
data-test-button="revert-to-draft"
>
<span>Unpublish and revert to private draft &rarr;</span>
</button>
{{/if}}
</p>
</div>
</div>
{{/let}}
</div>

View File

@ -0,0 +1,27 @@
import Component from '@glimmer/component';
import {task} from 'ember-concurrency';
export default class UpdateFlowModalComponent extends Component {
static modalOptions = {
className: 'fullscreen-modal-total-overlay publish-modal',
omitBackdrop: true,
ignoreBackdropClick: true
};
// We only show the newsletter name in the app if there's more than the single default newsletter.
// However, here we can show historic email data so it could have been sent to a now-archived
// newsletter in which case we want to force display of the newsletter name to avoid confusion.
get showNewsletterName() {
const {publishOptions} = this.args.data;
return !publishOptions.onlyDefaultNewsletter
|| publishOptions.post.newsletter?.status === 'archived';
}
@task
*saveTask() {
yield this.args.data.saveTask.perform();
this.args.close();
return true;
}
}

View File

@ -0,0 +1,72 @@
{{#if @publishManagement.publishOptions.user.isContributor}}
{{#if @publishManagement.post.isDraft}}
<button
type="button"
class="gh-btn gh-btn-editor gh-editor-preview-trigger"
{{on "click" @publishManagement.openPreview}}
{{on-key "cmd+p" @publishManagement.togglePreview}}
data-test-button="contributor-preview"
>
<span>Preview</span>
</button>
{{/if}}
<GhTaskButton
@buttonText="Save"
@task={{@publishManagement.saveTask}}
@runningText="Saving"
@class="gh-btn gh-btn-icon gh-btn-editor gh-editor-save-trigger contributor-save-button"
data-test-button="contributor-save" />
{{else}}
{{#if @publishManagement.post.isDraft}}
<button
type="button"
class="gh-btn gh-btn-editor gh-editor-preview-trigger"
{{on "click" @publishManagement.openPreview}}
{{on-key "cmd+p" @publishManagement.togglePreview}}
data-test-button="publish-preview"
>
<span>Preview</span>
</button>
<button
type="button"
class="gh-btn gh-btn-editor darkgrey gh-publish-trigger"
{{on "click" @publishManagement.openPublishFlow}}
{{on-key "cmd+shift+p"}}
disabled={{@publishManagement.publishOptions.isLoading}}
data-test-button="publish-flow"
>
<span>Publish</span>
</button>
{{else}}
<GhTaskButton
@task={{@publishManagement.saveTask}}
@buttonText="Update"
@runningText="Updating..."
@successText="Updated"
@class="gh-btn gh-btn-editor gh-editor-save-trigger gh-publish-trigger"
@idleClass="green"
@runningClass="midlightgrey"
@successClass="midlightgrey"
@failureClass="red"
@showIcon={{false}}
@disabled={{not @publishManagement.hasUnsavedChanges}}
@autoReset={{true}}
data-test-button="publish-save"
/>
{{#if (not (and @publishManagement.post.isSent (not @publishManagement.post.isPublished)))}}
<button
type="button"
class="gh-btn gh-btn-editor darkgrey gh-publish-trigger"
{{on "click" @publishManagement.openUpdateFlow}}
data-test-button="update-flow"
>
<span>
{{if @publishManagement.post.isScheduled "Unschedule" "Unpublish"}}
</span>
</button>
{{/if}}
{{/if}}
{{/if}}

View File

@ -0,0 +1,11 @@
{{yield (hash
post=@post
publishOptions=this.publishOptions
openPreview=this.openPreview
togglePreview=this.togglePreview
saveTask=this.saveTask
saveButtonTaskGroup=this.saveButtonTaskGroup
hasUnsavedChanges=@hasUnsavedChanges
openPublishFlow=this.openPublishFlow
openUpdateFlow=this.openUpdateFlow
)}}

Some files were not shown because too many files have changed in this diff Show More