diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 00000000..7898fbf8 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,3 @@ +{ + "extends": "eslint-config-discourse" +} diff --git a/.github/workflows/plugin-linting.yml b/.github/workflows/plugin-linting.yml new file mode 100644 index 00000000..a121658d --- /dev/null +++ b/.github/workflows/plugin-linting.yml @@ -0,0 +1,53 @@ +name: Linting + +on: + push: + branches: + - master + - main + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Set up Node.js + uses: actions/setup-node@v1 + with: + node-version: 12 + + - name: Set up ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 2.7 + + - name: Setup bundler + run: gem install bundler -v 2.1.4 --no-doc + + - name: Setup gems + run: bundle install --jobs 4 + + - name: Yarn install + run: yarn install --dev + + - name: ESLint + run: yarn eslint --ext .js,.js.es6 --no-error-on-unmatched-pattern {test,assets}/javascripts + + - name: Prettier + run: | + yarn prettier -v + if [ -d "assets" ]; then \ + yarn prettier --list-different "assets/**/*.{scss,js,es6}" ; \ + fi + if [ -d "test" ]; then \ + yarn prettier --list-different "test/**/*.{js,es6}" ; \ + fi + + - name: Ember template lint + run: yarn ember-template-lint assets/javascripts + + - name: Rubocop + run: bundle exec rubocop . \ No newline at end of file diff --git a/.github/workflows/plugin-tests.yml b/.github/workflows/plugin-tests.yml new file mode 100644 index 00000000..ce6112af --- /dev/null +++ b/.github/workflows/plugin-tests.yml @@ -0,0 +1,137 @@ +name: Plugin Tests + +on: + push: + branches: + - master + - main + pull_request: + +jobs: + build: + name: ${{ matrix.build_type }} + runs-on: ubuntu-latest + timeout-minutes: 60 + + env: + DISCOURSE_HOSTNAME: www.example.com + RUBY_GLOBAL_METHOD_CACHE_SIZE: 131072 + RAILS_ENV: test + PGHOST: localhost + PGUSER: discourse + PGPASSWORD: discourse + + strategy: + fail-fast: false + + matrix: + build_type: ["backend", "frontend"] + ruby: ["2.7"] + postgres: ["12"] + redis: ["4.x"] + + services: + postgres: + image: postgres:${{ matrix.postgres }} + ports: + - 5432:5432 + env: + POSTGRES_USER: discourse + POSTGRES_PASSWORD: discourse + options: >- + --mount type=tmpfs,destination=/var/lib/postgresql/data + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - uses: actions/checkout@v2 + with: + repository: discourse/discourse + fetch-depth: 1 + + - name: Install plugin + uses: actions/checkout@v2 + with: + path: plugins/${{ github.event.repository.name }} + fetch-depth: 1 + + - name: Check spec existence + id: check_spec + uses: andstor/file-existence-action@v1 + with: + files: "plugins/${{ github.event.repository.name }}/spec" + + - name: Check qunit existence + id: check_qunit + uses: andstor/file-existence-action@v1 + with: + files: "plugins/${{ github.event.repository.name }}/test/javascripts" + + - name: Setup Git + run: | + git config --global user.email "ci@ci.invalid" + git config --global user.name "Discourse CI" + + - name: Setup packages + run: | + sudo apt-get update + sudo apt-get -yqq install postgresql-client libpq-dev gifsicle jpegoptim optipng jhead + wget -qO- https://raw.githubusercontent.com/discourse/discourse_docker/master/image/base/install-pngquant | sudo sh + + - name: Update imagemagick + if: matrix.build_type == 'backend' + run: | + wget https://raw.githubusercontent.com/discourse/discourse_docker/master/image/base/install-imagemagick + chmod +x install-imagemagick + sudo ./install-imagemagick + + - name: Setup redis + uses: shogo82148/actions-setup-redis@v1 + with: + redis-version: ${{ matrix.redis }} + + - name: Setup ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true + + - name: Lint English locale + if: matrix.build_type == 'backend' + run: bundle exec ruby script/i18n_lint.rb "plugins/${{ github.event.repository.name }}/locales/{client,server}.en.yml" + + - name: Get yarn cache directory + id: yarn-cache-dir + run: echo "::set-output name=dir::$(yarn cache dir)" + + - name: Yarn cache + uses: actions/cache@v2 + id: yarn-cache + with: + path: ${{ steps.yarn-cache-dir.outputs.dir }} + key: ${{ runner.os }}-${{ matrix.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-${{ matrix.os }}-yarn- + + - name: Yarn install + run: yarn install --dev + + - name: Migrate database + run: | + bin/rake db:create + bin/rake db:migrate + + - name: Plugin RSpec + if: matrix.build_type == 'backend' && steps.check_spec.outputs.files_exists == 'true' + run: bin/rake plugin:spec[${{ github.event.repository.name }}] + + - name: Plugin QUnit + if: matrix.build_type == 'frontend' && steps.check_qunit.outputs.files_exists == 'true' + run: bundle exec rake plugin:qunit['${{ github.event.repository.name }}','1200000'] + timeout-minutes: 30 + + - name: Simplecov Report + if: matrix.build_type == 'backend' + run: COVERAGE=1 bin/rake plugin:spec[${{ github.event.repository.name }}] diff --git a/.gitignore b/.gitignore index 9c1559ac..11ce0a3c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,7 @@ coverage/* -!coverage/.last_run.json \ No newline at end of file +!coverage/.last_run.json +gems/ +.bundle/ +auto_generated +.DS_Store +node_modules/ diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/.prettierrc @@ -0,0 +1 @@ +{} diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 00000000..d46296cf --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,2 @@ +inherit_gem: + rubocop-discourse: default.yml diff --git a/.template-lintrc.js b/.template-lintrc.js new file mode 100644 index 00000000..a558b8e3 --- /dev/null +++ b/.template-lintrc.js @@ -0,0 +1,4 @@ +module.exports = { + plugins: ["ember-template-lint-plugin-discourse"], + extends: "discourse:recommended", +}; diff --git a/Gemfile b/Gemfile new file mode 100644 index 00000000..7da32ec0 --- /dev/null +++ b/Gemfile @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +source 'https://rubygems.org' + +group :development do + gem 'rubocop-discourse' +end diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 00000000..2416ce66 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,38 @@ +GEM + remote: https://rubygems.org/ + specs: + ast (2.4.2) + parallel (1.20.1) + parser (3.0.1.0) + ast (~> 2.4.1) + rainbow (3.0.0) + regexp_parser (2.1.1) + rexml (3.2.5) + rubocop (1.12.1) + parallel (~> 1.10) + parser (>= 3.0.0.0) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.8, < 3.0) + rexml + rubocop-ast (>= 1.2.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 1.4.0, < 3.0) + rubocop-ast (1.4.1) + parser (>= 2.7.1.5) + rubocop-discourse (2.4.1) + rubocop (>= 1.1.0) + rubocop-rspec (>= 2.0.0) + rubocop-rspec (2.2.0) + rubocop (~> 1.0) + rubocop-ast (>= 1.1.0) + ruby-progressbar (1.11.0) + unicode-display_width (2.0.0) + +PLATFORMS + ruby + +DEPENDENCIES + rubocop-discourse + +BUNDLED WITH + 2.2.16 diff --git a/assets/javascripts/discourse/components/custom-field-input.js.es6 b/assets/javascripts/discourse/components/custom-field-input.js.es6 index 1ad0b152..f2dca4c7 100644 --- a/assets/javascripts/discourse/components/custom-field-input.js.es6 +++ b/assets/javascripts/discourse/components/custom-field-input.js.es6 @@ -1,6 +1,7 @@ import Component from "@ember/component"; import discourseComputed, { observes } from "discourse-common/utils/decorators"; -import { or, alias } from "@ember/object/computed"; +import { alias, or } from "@ember/object/computed"; +import I18n from "I18n"; const generateContent = function (array, type) { return array.map((key) => ({ @@ -34,7 +35,7 @@ export default Component.extend({ }, @discourseComputed("field.klass") - serializerContent(klass, p2) { + serializerContent(klass) { const serializers = this.get(`${klass}Serializers`); if (serializers) { @@ -66,21 +67,27 @@ export default Component.extend({ "field.serializers" ) saveDisabled(saving) { - if (saving) return true; + if (saving) { + return true; + } const originalField = this.originalField; - if (!originalField) return false; + if (!originalField) { + return false; + } return ["name", "klass", "type", "serializers"].every((attr) => { let current = this.get(attr); let original = originalField[attr]; - if (!current) return false; + if (!current) { + return false; + } - if (attr == "serializers") { + if (attr === "serializers") { return this.compareArrays(current, original); } else { - return current == original; + return current === original; } }); }, diff --git a/assets/javascripts/discourse/components/wizard-advanced-toggle.js.es6 b/assets/javascripts/discourse/components/wizard-advanced-toggle.js.es6 index b03a7ce5..c6e1fd9c 100644 --- a/assets/javascripts/discourse/components/wizard-advanced-toggle.js.es6 +++ b/assets/javascripts/discourse/components/wizard-advanced-toggle.js.es6 @@ -7,7 +7,9 @@ export default Component.extend({ @discourseComputed("showAdvanced") toggleClass(showAdvanced) { let classes = "btn"; - if (showAdvanced) classes += " btn-primary"; + if (showAdvanced) { + classes += " btn-primary"; + } return classes; }, diff --git a/assets/javascripts/discourse/components/wizard-custom-action.js.es6 b/assets/javascripts/discourse/components/wizard-custom-action.js.es6 index 3dcc85d1..c8309f10 100644 --- a/assets/javascripts/discourse/components/wizard-custom-action.js.es6 +++ b/assets/javascripts/discourse/components/wizard-custom-action.js.es6 @@ -1,11 +1,10 @@ import { default as discourseComputed } from "discourse-common/utils/decorators"; -import { equal, empty, or, and } from "@ember/object/computed"; -import { generateName, selectKitContent } from "../lib/wizard"; +import { and, empty, equal, or } from "@ember/object/computed"; +import { notificationLevels, selectKitContent } from "../lib/wizard"; import { computed } from "@ember/object"; import wizardSchema from "../lib/wizard-schema"; import UndoChanges from "../mixins/undo-changes"; import Component from "@ember/component"; -import { notificationLevels } from "../lib/wizard"; import I18n from "I18n"; export default Component.extend(UndoChanges, { @@ -43,7 +42,7 @@ export default Component.extend(UndoChanges, { name: I18n.t(`admin.wizard.action.${type}.label`), }; }), - availableNotificationLevels: notificationLevels.map((type, index) => { + availableNotificationLevels: notificationLevels.map((type) => { return { id: type, name: I18n.t( @@ -92,7 +91,9 @@ export default Component.extend(UndoChanges, { @discourseComputed("apis", "action.api") availableEndpoints(apis, api) { - if (!api) return []; + if (!api) { + return []; + } return apis.find((a) => a.name === api).endpoints; }, }); diff --git a/assets/javascripts/discourse/components/wizard-custom-field.js.es6 b/assets/javascripts/discourse/components/wizard-custom-field.js.es6 index 85c26677..b5f6b0ee 100644 --- a/assets/javascripts/discourse/components/wizard-custom-field.js.es6 +++ b/assets/javascripts/discourse/components/wizard-custom-field.js.es6 @@ -1,5 +1,5 @@ import { default as discourseComputed } from "discourse-common/utils/decorators"; -import { equal, or, alias } from "@ember/object/computed"; +import { alias, equal, or } from "@ember/object/computed"; import { computed } from "@ember/object"; import { selectKitContent } from "../lib/wizard"; import UndoChanges from "../mixins/undo-changes"; @@ -107,6 +107,40 @@ export default Component.extend(UndoChanges, { return this.setupTypeOutput(fieldType, options); }, + @discourseComputed("step.index") + fieldConditionOptions(stepIndex) { + const options = { + inputTypes: "validation", + context: "field", + textSelection: "value", + userFieldSelection: true, + groupSelection: true, + }; + + if (stepIndex > 0) { + options.wizardFieldSelection = true; + options.wizardActionSelection = true; + } + + return options; + }, + + @discourseComputed("step.index") + fieldIndexOptions(stepIndex) { + const options = { + context: "field", + userFieldSelection: true, + groupSelection: true, + }; + + if (stepIndex > 0) { + options.wizardFieldSelection = true; + options.wizardActionSelection = true; + } + + return options; + }, + actions: { imageUploadDone(upload) { this.set("field.image", upload.url); diff --git a/assets/javascripts/discourse/components/wizard-custom-step.js.es6 b/assets/javascripts/discourse/components/wizard-custom-step.js.es6 index 102af717..2a07dd65 100644 --- a/assets/javascripts/discourse/components/wizard-custom-step.js.es6 +++ b/assets/javascripts/discourse/components/wizard-custom-step.js.es6 @@ -1,9 +1,27 @@ import Component from "@ember/component"; -import { default as discourseComputed } from "discourse-common/utils/decorators"; +import discourseComputed from "discourse-common/utils/decorators"; export default Component.extend({ classNames: "wizard-custom-step", + @discourseComputed("step.index") + stepConditionOptions(stepIndex) { + const options = { + inputTypes: "validation", + context: "step", + textSelection: "value", + userFieldSelection: true, + groupSelection: true, + }; + + if (stepIndex > 0) { + options["wizardFieldSelection"] = true; + options["wizardActionSelection"] = true; + } + + return options; + }, + actions: { bannerUploadDone(upload) { this.set("step.banner", upload.url); diff --git a/assets/javascripts/discourse/components/wizard-links.js.es6 b/assets/javascripts/discourse/components/wizard-links.js.es6 index 6f2ca117..c32809aa 100644 --- a/assets/javascripts/discourse/components/wizard-links.js.es6 +++ b/assets/javascripts/discourse/components/wizard-links.js.es6 @@ -1,15 +1,15 @@ import { default as discourseComputed, - on, observes, + on, } from "discourse-common/utils/decorators"; import { generateName } from "../lib/wizard"; import { - default as wizardSchema, setWizardDefaults, + default as wizardSchema, } from "../lib/wizard-schema"; import { notEmpty } from "@ember/object/computed"; -import { scheduleOnce, bind } from "@ember/runloop"; +import { scheduleOnce } from "@ember/runloop"; import EmberObject from "@ember/object"; import Component from "@ember/component"; import { A } from "@ember/array"; @@ -38,6 +38,7 @@ export default Component.extend({ const items = this.items; const item = items.findBy("id", itemId); items.removeObject(item); + item.set("index", newIndex); items.insertAt(newIndex, item); scheduleOnce("afterRender", this, () => this.applySortable()); }, @@ -53,7 +54,9 @@ export default Component.extend({ "items.@each.title" ) links(current, items) { - if (!items) return; + if (!items) { + return; + } return items.map((item) => { if (item) { @@ -88,22 +91,14 @@ export default Component.extend({ params.isNew = true; - let next = 1; - + let index = 0; if (items.length) { - next = - Math.max.apply( - Math, - items.map((i) => { - let parts = i.id.split("_"); - let lastPart = parts[parts.length - 1]; - return isNaN(lastPart) ? 0 : lastPart; - }) - ) + 1; + index = items.length; } - let id = `${itemType}_${next}`; + params.index = index; + let id = `${itemType}_${index + 1}`; if (itemType === "field") { id = `${this.parentId}_${id}`; } diff --git a/assets/javascripts/discourse/components/wizard-mapper-connector.js.es6 b/assets/javascripts/discourse/components/wizard-mapper-connector.js.es6 index 36c0ec20..3ec2d502 100644 --- a/assets/javascripts/discourse/components/wizard-mapper-connector.js.es6 +++ b/assets/javascripts/discourse/components/wizard-mapper-connector.js.es6 @@ -3,7 +3,6 @@ import { gt } from "@ember/object/computed"; import { computed } from "@ember/object"; import { defaultConnector } from "../lib/wizard-mapper"; import { later } from "@ember/runloop"; -import { observes } from "discourse-common/utils/decorators"; import I18n from "I18n"; export default Component.extend({ diff --git a/assets/javascripts/discourse/components/wizard-mapper-input.js.es6 b/assets/javascripts/discourse/components/wizard-mapper-input.js.es6 index c7327fbc..021f2084 100644 --- a/assets/javascripts/discourse/components/wizard-mapper-input.js.es6 +++ b/assets/javascripts/discourse/components/wizard-mapper-input.js.es6 @@ -1,11 +1,11 @@ import { computed, set } from "@ember/object"; -import { alias, equal, or, not } from "@ember/object/computed"; +import { alias, equal, not, or } from "@ember/object/computed"; import { - newPair, connectorContent, - inputTypesContent, - defaultSelectionType, defaultConnector, + defaultSelectionType, + inputTypesContent, + newPair, } from "../lib/wizard-mapper"; import Component from "@ember/component"; import { observes } from "discourse-common/utils/decorators"; diff --git a/assets/javascripts/discourse/components/wizard-mapper-pair.js.es6 b/assets/javascripts/discourse/components/wizard-mapper-pair.js.es6 index bc7e9be9..cb237056 100644 --- a/assets/javascripts/discourse/components/wizard-mapper-pair.js.es6 +++ b/assets/javascripts/discourse/components/wizard-mapper-pair.js.es6 @@ -1,6 +1,6 @@ import { connectorContent } from "../lib/wizard-mapper"; -import { gt, or, alias } from "@ember/object/computed"; -import { computed, observes } from "@ember/object"; +import { alias, gt } from "@ember/object/computed"; +import { computed } from "@ember/object"; import Component from "@ember/component"; export default Component.extend({ diff --git a/assets/javascripts/discourse/components/wizard-mapper-selector.js.es6 b/assets/javascripts/discourse/components/wizard-mapper-selector.js.es6 index e70708c9..6d65d782 100644 --- a/assets/javascripts/discourse/components/wizard-mapper-selector.js.es6 +++ b/assets/javascripts/discourse/components/wizard-mapper-selector.js.es6 @@ -1,13 +1,12 @@ -import { alias, or, gt } from "@ember/object/computed"; +import { alias, gt, or } from "@ember/object/computed"; import { computed } from "@ember/object"; import { default as discourseComputed, observes, - on, } from "discourse-common/utils/decorators"; import { getOwner } from "discourse-common/lib/get-owner"; import { defaultSelectionType, selectionTypes } from "../lib/wizard-mapper"; -import { snakeCase, generateName, userProperties } from "../lib/wizard"; +import { generateName, snakeCase, userProperties } from "../lib/wizard"; import Component from "@ember/component"; import { bind, later } from "@ember/runloop"; import I18n from "I18n"; @@ -135,7 +134,9 @@ export default Component.extend({ }, documentClick(e) { - if (this._state == "destroying") return; + if (this._state === "destroying") { + return; + } let $target = $(e.target); if (!$target.parents(".type-selector").length && this.showTypes) { @@ -249,7 +250,7 @@ export default Component.extend({ }, @discourseComputed("activeType", "inputType") - placeholderKey(activeType, inputType) { + placeholderKey(activeType) { if ( activeType === "text" && this.options[`${this.selectorType}Placeholder`] @@ -275,14 +276,20 @@ export default Component.extend({ optionEnabled(type) { const options = this.options; - if (!options) return false; + if (!options) { + return false; + } const option = options[type]; - if (option === true) return true; - if (typeof option !== "string") return false; + if (option === true) { + return true; + } + if (typeof option !== "string") { + return false; + } - return option.split(",").filter((option) => { - return [this.selectorType, this.inputType].indexOf(option) !== -1; + return option.split(",").filter((o) => { + return [this.selectorType, this.inputType].indexOf(o) !== -1; }).length; }, diff --git a/assets/javascripts/discourse/components/wizard-mapper.js.es6 b/assets/javascripts/discourse/components/wizard-mapper.js.es6 index 7581adbd..95aabb1c 100644 --- a/assets/javascripts/discourse/components/wizard-mapper.js.es6 +++ b/assets/javascripts/discourse/components/wizard-mapper.js.es6 @@ -1,10 +1,5 @@ -import { getOwner } from "discourse-common/lib/get-owner"; import { newInput, selectionTypes } from "../lib/wizard-mapper"; -import { - default as discourseComputed, - observes, - on, -} from "discourse-common/utils/decorators"; +import discourseComputed from "discourse-common/utils/decorators"; import { later } from "@ember/runloop"; import Component from "@ember/component"; import { A } from "@ember/array"; diff --git a/assets/javascripts/discourse/components/wizard-realtime-validations.js.es6 b/assets/javascripts/discourse/components/wizard-realtime-validations.js.es6 index b9b36e6f..8332b86e 100644 --- a/assets/javascripts/discourse/components/wizard-realtime-validations.js.es6 +++ b/assets/javascripts/discourse/components/wizard-realtime-validations.js.es6 @@ -19,7 +19,9 @@ export default Component.extend({ init() { this._super(...arguments); - if (!this.validations) return; + if (!this.validations) { + return; + } if (!this.field.validations) { const validations = {}; diff --git a/assets/javascripts/discourse/components/wizard-text-editor.js.es6 b/assets/javascripts/discourse/components/wizard-text-editor.js.es6 index 2866537d..88d7200c 100644 --- a/assets/javascripts/discourse/components/wizard-text-editor.js.es6 +++ b/assets/javascripts/discourse/components/wizard-text-editor.js.es6 @@ -1,7 +1,4 @@ -import { - default as discourseComputed, - on, -} from "discourse-common/utils/decorators"; +import discourseComputed from "discourse-common/utils/decorators"; import { notEmpty } from "@ember/object/computed"; import { userProperties } from "../lib/wizard"; import { scheduleOnce } from "@ember/runloop"; diff --git a/assets/javascripts/discourse/connectors/admin-menu/wizards-nav-button.hbs b/assets/javascripts/discourse/connectors/admin-menu/wizards-nav-button.hbs index 5398e27d..f76722fc 100644 --- a/assets/javascripts/discourse/connectors/admin-menu/wizards-nav-button.hbs +++ b/assets/javascripts/discourse/connectors/admin-menu/wizards-nav-button.hbs @@ -1,3 +1,3 @@ {{#if currentUser.admin}} - {{nav-item route='adminWizards' label='admin.wizard.nav_label'}} + {{nav-item route="adminWizards" label="admin.wizard.nav_label"}} {{/if}} diff --git a/assets/javascripts/discourse/connectors/top-notices/prompt-completion.hbs b/assets/javascripts/discourse/connectors/top-notices/prompt-completion.hbs index 503ee113..70c0b7c4 100644 --- a/assets/javascripts/discourse/connectors/top-notices/prompt-completion.hbs +++ b/assets/javascripts/discourse/connectors/top-notices/prompt-completion.hbs @@ -1,7 +1,7 @@ {{#each site.complete_custom_wizard as |wizard|}} -