diff --git a/.browserslistrc b/.browserslistrc new file mode 100644 index 0000000000..3ae7766c32 --- /dev/null +++ b/.browserslistrc @@ -0,0 +1,16 @@ +# +# This list of browsers is a conservative first definition, based on +# https://docs.gitlab.com/ee/install/requirements.html#supported-web-browsers +# with the following reasoning: +# +# - Edge: Pick the last two major version before the Chrome switch +# - Rest: We should support the latest ESR of Firefox: 68, because it used quite a lot. +# For the rest, pick browser versions that have a similar age to Firefox 68. +# +# See also this follow-up epic: +# https://gitlab.com/groups/gitlab-org/-/epics/3957 +# +chrome >= 73 +edge >= 17 +firefox >= 68 +safari >= 12 diff --git a/.eslintrc.yml b/.eslintrc.yml index a764f74978..75c52ac131 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -42,21 +42,50 @@ rules: no-jquery/no-serialize: error promise/always-return: off promise/no-callback-in-promise: off - "@gitlab/no-global-event-off": error - - # BEGIN eslint-plugin-vue@7 overrides - # TODO: Remove these rules as part of - # https://gitlab.com/groups/gitlab-org/-/epics/5142. These are setting - # various vue lint rules as they were in eslint-plugin-vue@6, or disabling - # new ones, to ease migration to v7, so violations of each can be fixed - # separately. - vue/no-mutating-props: off - vue/one-component-per-file: off - vue/no-lone-template: off - vue/component-definition-name-casing: off - # END eslint-plugin-vue@7 overrides + '@gitlab/no-global-event-off': error + import/order: + - error + - groups: + - builtin + - external + - internal + - parent + - sibling + - index + pathGroups: + - pattern: ~/** + group: internal + - pattern: emojis/** + group: internal + - pattern: '{ee_,}empty_states/**' + group: internal + - pattern: '{ee_,}icons/**' + group: internal + - pattern: '{ee_,}images/**' + group: internal + - pattern: vendor/** + group: internal + - pattern: shared_queries/** + group: internal + - pattern: '{ee_,}spec/**' + group: internal + - pattern: '{ee_,}jest/**' + group: internal + - pattern: ee_else_ce/** + group: internal + - pattern: ee/** + group: internal + - pattern: ee_component/** + group: internal + - pattern: '{test_,}helpers/**' + group: internal + - pattern: test_fixtures/** + group: internal + alphabetize: + order: asc overrides: - files: - '**/spec/**/*' rules: - "@gitlab/require-i18n-strings": off + '@gitlab/require-i18n-strings': off + '@gitlab/no-runtime-template-compiler': off diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b41d7bcd34..e38e2f765b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -17,7 +17,7 @@ stages: # in cases where jobs require Docker-in-Docker, the job # definition must be extended with `.use-docker-in-docker` default: - image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7.2-golang-1.14-git-2.29-lfs-2.9-chrome-85-node-12.18-yarn-1.22-postgresql-11-graphicsmagick-1.3.34" + image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7.2.patched-golang-1.14-git-2.29-lfs-2.9-chrome-87-node-14.15-yarn-1.22-postgresql-11-graphicsmagick-1.3.34" tags: - gitlab-org # All jobs are interruptible by default @@ -111,3 +111,4 @@ include: - local: .gitlab/ci/dast.gitlab-ci.yml - local: .gitlab/ci/workhorse.gitlab-ci.yml - local: .gitlab/ci/graphql.gitlab-ci.yml + - local: .gitlab/ci/verify-lockfile.gitlab-ci.yml diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS index df81d05eec..0d34eeccf8 100644 --- a/.gitlab/CODEOWNERS +++ b/.gitlab/CODEOWNERS @@ -6,137 +6,128 @@ *.rb @gitlab-org/maintainers/rails-backend *.rake @gitlab-org/maintainers/rails-backend -[Documentation] +[Documentation Directories] /doc/ @gl-docsteam -/doc/administration/monitoring/ @aqualls +/doc/.vale/ @marcel.amirault @eread @aqualls @cnorris +/doc/administration/geo/ @axil +/doc/administration/gitaly/ @eread +/doc/administration/integration/ @aqualls +/doc/administration/lfs/ @aqualls +/doc/administration/monitoring/ @ngaskill +/doc/administration/operations/ @axil @eread @marcia +/doc/administration/packages/ @ngaskill +/doc/administration/postgresql/ @marcia +/doc/administration/raketasks/ @axil @eread @mjang1 +/doc/administration/redis/ @axil +/doc/administration/reference_architectures/ @axil +/doc/administration/snippets/ @aqualls +/doc/administration/troubleshooting @axil @marcia @mjang1 +/doc/ci/ @marcel.amirault @sselhorn +/doc/ci/environments/ @axil +/doc/ci/release/ @axil +/doc/ci/services/ @sselhorn +/doc/ci/test_cases/ @msedlakjakubowski /doc/development/ @marcia @mjang1 /doc/development/documentation/ @cnorris -/doc/ci @marcel.amirault @sselhorn -/doc/operations @aqualls @eread -/doc/user/clusters @aqualls -/doc/user/infrastructure @aqualls -/doc/user/project/clusters @aqualls -/doc/.vale/ @marcel.amirault @eread @aqualls @cnorris +/doc/gitlab-basics/ @marcia +/doc/install/ @axil +/doc/integration/ @aqualls @mjang1 +/doc/operations/ @ngaskill @axil +/doc/push_rules/ @aqualls +/doc/ssh/ @mjang1 +/doc/subscriptions/ @sselhorn +/doc/topics/autodevops/ @ngaskill @marcia +/doc/topics/git/ @aqualls +/doc/update/ @axil @marcia +/doc/user/analytics/ @mjang1 @ngaskill +/doc/user/application_security @rdickenson +/doc/user/clusters/ @marcia +/doc/user/compliance/ @mjang1 @rdickenson +/doc/user/group/ @mjang1 @msedlakjakubowski +/doc/user/group/bulk_editing/ @msedlakjakubowski +/doc/user/group/epics/ @msedlakjakubowski +/doc/user/group/iterations/ @msedlakjakubowski +/doc/user/group/roadmap/ @msedlakjakubowski +/doc/user/infrastructure/ @marcia +/doc/user/packages/ @ngaskill +/doc/user/profile/ @mjang1 @msedlakjakubowski +/doc/user/project/ @aqualls @axil @eread @mjang1 @msedlakjakubowski @ngaskill +/doc/user/project/clusters/ @ngaskill +/doc/user/project/import/ @mjang1 @msedlakjakubowski +/doc/user/project/integrations/ @aqualls +/doc/user/project/integrations/prometheus_library/ @ngaskill +/doc/user/project/issues/ @msedlakjakubowski +/doc/user/project/merge_requests/ @aqualls @eread +/doc/user/project/milestones/ @msedlakjakubowski +/doc/user/project/pages/ @axil +/doc/user/project/repository/ @aqualls +/doc/user/project/settings/ @mjang1 @aqualls +/doc/user/project/static_site_editor/index.md @aqualls +/doc/user/project/web_ide/index.md @aqualls +/doc/user/project/wiki/index.md @aqualls +/doc/user/search/ @marcia @aqualls [Docs Create] -/doc/user/project/merge_requests/allow_collaboration.md @marcia -/doc/user/project/merge_requests/authorization_for_merge_requests.md @marcia -/doc/user/project/merge_requests/cherry_pick_changes.md @marcia -/doc/user/project/merge_requests/creating_merge_requests.md @marcia -/doc/user/project/merge_requests/fast_forward_merge.md @marcia -/doc/user/project/merge_requests/getting_started.md @marcia -/doc/user/project/merge_requests/index.md @marcia -/doc/user/project/merge_requests/merge_request_approvals.md @marcia -/doc/user/project/merge_requests/merge_request_dependencies.md @marcia -/doc/user/project/merge_requests/resolve_conflicts.md @marcia -/doc/user/project/merge_requests/revert_changes.md @marcia -/doc/user/project/merge_requests/reviewing_and_managing_merge_requests.md @marcia -/doc/user/project/merge_requests/squash_and_merge.md @marcia -/doc/user/project/merge_requests/work_in_progress_merge_requests.md @marcia -/doc/user/project/repository/file_finder.md @marcia -/doc/user/project/repository/forking_workflow.md @marcia -/doc/user/project/repository/git_blame.md @marcia -/doc/user/project/repository/git_history.md @marcia -/doc/user/project/repository/index.md @marcia -/doc/user/project/repository/repository_mirroring.md @marcia -/doc/user/project/repository/web_editor.md @marcia -/doc/user/project/autocomplete_characters.md @marcia -/doc/user/project/badges.md @marcia -/doc/user/project/code_intelligence.md @marcia -/doc/user/project/code_owners.md @marcia -/doc/user/project/file_lock.md @marcia -/doc/user/project/git_attributes.md @marcia -/doc/user/project/highlighting.md @marcia -/doc/user/project/index.md @marcia -/doc/user/project/protected_branches.md @marcia -/doc/user/project/protected_tags.md @marcia -/doc/user/project/push_options.md @marcia -/doc/user/project/repository/branches/index.md @marcia -/doc/user/project/repository/gpg_signed_commits/index.md @marcia -/doc/user/project/repository/jupyter_notebooks/index.md @marcia -/doc/user/project/repository/x509_signed_commits/index.md @marcia -/doc/user/project/settings/import_export.md @marcia -/doc/user/project/settings/index.md @marcia -/doc/user/project/settings/project_access_tokens.md @marcia -/doc/user/project/static_site_editor/index.md @marcia -/doc/user/project/web_ide/index.md @marcia -/doc/user/project/wiki/index.md @marcia -/doc/gitlab-basics/README.md @marcia -/doc/gitlab-basics/add-file.md @marcia -/doc/gitlab-basics/command-line-commands.md @marcia -/doc/gitlab-basics/create-branch.md @marcia -/doc/gitlab-basics/create-project.md @marcia -/doc/gitlab-basics/create-your-ssh-keys.md @marcia -/doc/gitlab-basics/feature_branch_workflow.md @marcia -/doc/gitlab-basics/fork-project.md @marcia -/doc/gitlab-basics/start-using-git.md @marcia -/doc/integration/sourcegraph.md @marcia -/doc/intro/README.md @marcia -/doc/push_rules/push_rules.md @marcia -/doc/ssh/README.md @marcia -/doc/topics/git/feature_branch_development.md @marcia -/doc/topics/git/how_to_install_git/index.md @marcia -/doc/topics/git/index.md @marcia -/doc/topics/git/lfs/index.md @marcia -/doc/topics/git/lfs/migrate_from_git_annex_to_git_lfs.md @marcia -/doc/topics/git/numerous_undo_possibilities_in_git/index.md @marcia -/doc/topics/git/partial_clone.md @marcia -/doc/topics/git/troubleshooting_git.md @marcia -/doc/topics/git/useful_git_commands.md @marcia -/doc/topics/gitlab_flow.md @marcia -/doc/user/index.md @marcia -/doc/user/snippets.md @marcia -/doc/administration/issue_closing_pattern.md @marcia -/doc/user/asciidoc.md @marcia -/doc/user/markdown.md @marcia -/doc/user/search/advanced_global_search.md @marcia -/doc/user/search/advanced_search_syntax.md @marcia -/doc/user/search/index.md @marcia -/doc/administration/file_hooks.md @marcia -/doc/administration/git_annex.md @marcia -/doc/administration/git_protocol.md @marcia -/doc/administration/integration/plantuml.md @marcia -/doc/administration/invalidate_markdown_cache.md @marcia -/doc/administration/issue_closing_pattern.md @marcia -/doc/administration/lfs/index.md @marcia -/doc/administration/merge_request_diffs.md @marcia -/doc/administration/repository_checks.md @marcia -/doc/administration/snippets/index.md @marcia -/doc/administration/static_objects_external_storage.md @marcia -/doc/api/access_requests.md @marcia -/doc/api/branches.md @marcia -/doc/api/commits.md @marcia -/doc/api/discussions.md @marcia -/doc/api/group_wikis.md @marcia -/doc/api/keys.md @marcia -/doc/api/markdown.md @marcia -/doc/api/merge_request_approvals.md @marcia -/doc/api/merge_request_context_commits.md @marcia -/doc/api/merge_requests.md @marcia -/doc/api/project_aliases.md @marcia -/doc/api/project_badges.md @marcia -/doc/api/project_import_export.md @marcia -/doc/api/project_level_variables.md @marcia -/doc/api/project_snippets.md @marcia -/doc/api/project_statistics.md @marcia -/doc/api/project_templates.md @marcia -/doc/api/project_vulnerabilities.md @marcia -/doc/api/protected_branches.md @marcia -/doc/api/protected_tags.md @marcia -/doc/api/remote_mirrors.md @marcia -/doc/api/repositories.md @marcia -/doc/api/repository_files.md @marcia -/doc/api/repository_submodules.md @marcia -/doc/api/search.md @marcia -/doc/api/snippets.md @marcia -/doc/api/suggestions.md @marcia -/doc/api/tags.md @marcia -/doc/api/visual_review_discussions.md @marcia -/doc/api/wikis.md @marcia -/doc/user/admin_area/settings/account_and_limit_settings.md @marcia -/doc/user/admin_area/settings/instance_template_repository.md @marcia -/doc/user/admin_area/settings/push_event_activities_limit.md @marcia -/doc/user/admin_area/settings/visibility_and_access_controls.md @marcia +/doc/administration/file_hooks.md @aqualls +/doc/administration/git_annex.md @aqualls +/doc/administration/git_protocol.md @aqualls +/doc/administration/invalidate_markdown_cache.md @aqualls +/doc/administration/issue_closing_pattern.md @aqualls +/doc/administration/merge_request_diffs.md @aqualls +/doc/administration/repository_checks.md @aqualls +/doc/administration/static_objects_external_storage.md @aqualls +/doc/api/access_requests.md @aqualls +/doc/api/branches.md @aqualls +/doc/api/commits.md @aqualls +/doc/api/discussions.md @aqualls +/doc/api/group_wikis.md @aqualls +/doc/api/keys.md @aqualls +/doc/api/markdown.md @aqualls +/doc/api/merge_request_approvals.md @aqualls +/doc/api/merge_request_context_commits.md @aqualls +/doc/api/merge_requests.md @aqualls +/doc/api/project_aliases.md @aqualls +/doc/api/project_badges.md @aqualls +/doc/api/project_import_export.md @aqualls +/doc/api/project_level_variables.md @aqualls +/doc/api/project_snippets.md @aqualls +/doc/api/project_statistics.md @aqualls +/doc/api/project_templates.md @aqualls +/doc/api/project_vulnerabilities.md @aqualls +/doc/api/protected_branches.md @aqualls +/doc/api/protected_tags.md @aqualls +/doc/api/remote_mirrors.md @aqualls +/doc/api/repositories.md @aqualls +/doc/api/repository_files.md @aqualls +/doc/api/repository_submodules.md @aqualls +/doc/api/search.md @aqualls +/doc/api/snippets.md @aqualls +/doc/api/suggestions.md @aqualls +/doc/api/tags.md @aqualls +/doc/api/visual_review_discussions.md @aqualls +/doc/api/wikis.md @aqualls +/doc/intro/README.md @aqualls +/doc/topics/gitlab_flow.md @aqualls +/doc/user/admin_area/settings/account_and_limit_settings.md @aqualls +/doc/user/admin_area/settings/instance_template_repository.md @aqualls +/doc/user/admin_area/settings/push_event_activities_limit.md @aqualls +/doc/user/admin_area/settings/visibility_and_access_controls.md @aqualls +/doc/user/asciidoc.md @aqualls +/doc/user/index.md @aqualls +/doc/user/markdown.md @aqualls +/doc/user/project/autocomplete_characters.md @aqualls +/doc/user/project/badges.md @aqualls +/doc/user/project/code_intelligence.md @aqualls +/doc/user/project/code_owners.md @aqualls +/doc/user/project/file_lock.md @aqualls +/doc/user/project/git_attributes.md @aqualls +/doc/user/project/highlighting.md @aqualls +/doc/user/project/index.md @aqualls +/doc/user/project/protected_branches.md @aqualls +/doc/user/project/protected_tags.md @aqualls +/doc/user/project/push_options.md @aqualls +/doc/user/project/settings/import_export.md @aqualls +/doc/user/snippets.md @aqualls [Frontend] *.scss @annabeldunstone @gitlab-org/maintainers/frontend @@ -173,7 +164,7 @@ /.gitlab/CODEOWNERS @gl-quality/eng-prod Dangerfile @gl-quality/eng-prod /danger/ @gl-quality/eng-prod -/lib/gitlab/danger/ @gl-quality/eng-prod +/tooling/danger/ @gl-quality/eng-prod /scripts/ @gl-quality/eng-prod /scripts/frontend/ @gl-quality/eng-prod @gitlab-org/maintainers/frontend /scripts/review_apps/seed-dast-test-data.sh @dappelt @ngeorge1 @gl-quality/eng-prod @@ -257,3 +248,21 @@ Dangerfile @gl-quality/eng-prod /lib/gitlab/usage_data.rb @gitlab-org/growth/product_intelligence/engineers /lib/gitlab/cycle_analytics/usage_data.rb @gitlab-org/growth/product-intelligence/engineers /lib/gitlab/usage_data_counters/ @gitlab-org/growth/product-intelligence/engineers + +[Growth Experiments] +/app/assets/javascripts/lib/utils/experimentation.js @gitlab-org/growth/experiment-devs +/app/experiments/ @gitlab-org/growth/experiment-devs +/app/models/experiment.rb @gitlab-org/growth/experiment-devs +/app/models/experiment_subject.rb @gitlab-org/growth/experiment-devs +/app/models/experiment_user.rb @gitlab-org/growth/experiment-devs +/app/workers/experiments/ @gitlab-org/growth/experiment-devs +/config/feature_flags/experiment/ @gitlab-org/growth/experiment-devs +/ee/config/feature_flags/experiment/ @gitlab-org/growth/experiment-devs +/ee/lib/api/experiments.rb @gitlab-org/growth/experiment-devs +/ee/lib/ee/api/entities/experiment.rb @gitlab-org/growth/experiment-devs +/lib/gitlab/experimentation/ @gitlab-org/growth/experiment-devs +/lib/gitlab/experimentation.rb @gitlab-org/growth/experiment-devs +/lib/gitlab/experimentation_logger.rb @gitlab-org/growth/experiment-devs + +[Legal] +/config/dependency_decisions.yml @gitlab-org/legal-reviewers diff --git a/.gitlab/ci/cng.gitlab-ci.yml b/.gitlab/ci/cng.gitlab-ci.yml index 269996dfd0..af735d3212 100644 --- a/.gitlab/ci/cng.gitlab-ci.yml +++ b/.gitlab/ci/cng.gitlab-ci.yml @@ -1,6 +1,6 @@ cloud-native-image: extends: .cng:rules - image: ruby:2.7-alpine + image: ${GITLAB_DEPENDENCY_PROXY}ruby:2.7-alpine dependencies: [] stage: post-test variables: diff --git a/.gitlab/ci/docs.gitlab-ci.yml b/.gitlab/ci/docs.gitlab-ci.yml index 955f44c621..b42b32ea44 100644 --- a/.gitlab/ci/docs.gitlab-ci.yml +++ b/.gitlab/ci/docs.gitlab-ci.yml @@ -2,7 +2,7 @@ extends: - .default-retry - .docs:rules:review-docs - image: ruby:2.7-alpine + image: ${GITLAB_DEPENDENCY_PROXY}ruby:2.7-alpine stage: review needs: [] variables: @@ -66,13 +66,6 @@ docs-lint links: - bundle exec nanoc # Check the internal links - bundle exec nanoc check internal_links - # Delete the redirect files, rebuild, and check internal links again, to see if we are linking to redirects. - # Don't delete the documentation/index.md, which is a false positive for the simple grep. - - grep -rl "redirect_to:" /tmp/gitlab-docs/content/ee/ | grep -v "development/documentation/index.md" | xargs rm -f - - bundle exec nanoc - - echo -e "\e[1;96mThe following test fails when a doc links to a redirect file." - - echo -e "\e[1;96mMake sure all links point to the correct page." - - bundle exec nanoc check internal_links # Check the internal anchor links - bundle exec nanoc check internal_anchors diff --git a/.gitlab/ci/frontend.gitlab-ci.yml b/.gitlab/ci/frontend.gitlab-ci.yml index c87305cab1..1b4b8a1277 100644 --- a/.gitlab/ci/frontend.gitlab-ci.yml +++ b/.gitlab/ci/frontend.gitlab-ci.yml @@ -15,7 +15,7 @@ extends: - .frontend-base - .assets-compile-cache - image: registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7.2-git-2.29-lfs-2.9-node-12.18-yarn-1.22-graphicsmagick-1.3.34 + image: registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7.2-git-2.29-lfs-2.9-node-14.15-yarn-1.22-graphicsmagick-1.3.34 variables: WEBPACK_VENDOR_DLL: "true" stage: prepare @@ -259,13 +259,13 @@ coverage-frontend: qa-frontend-node:10: extends: .qa-frontend-node - image: node:dubnium + image: ${GITLAB_DEPENDENCY_PROXY}node:dubnium qa-frontend-node:latest: extends: - .qa-frontend-node - .frontend:rules:qa-frontend-node-latest - image: node:latest + image: ${GITLAB_DEPENDENCY_PROXY}node:latest webpack-dev-server: extends: diff --git a/.gitlab/ci/global.gitlab-ci.yml b/.gitlab/ci/global.gitlab-ci.yml index 355607c17a..5de8a6bc25 100644 --- a/.gitlab/ci/global.gitlab-ci.yml +++ b/.gitlab/ci/global.gitlab-ci.yml @@ -16,15 +16,23 @@ - source scripts/utils.sh - source scripts/prepare_build.sh -.rails-cache: +.setup-test-env-cache: cache: - key: "rails-v3" + key: "setup-test-env-v1" paths: - vendor/ruby/ - vendor/gitaly-ruby/ - .go/pkg/mod/ policy: pull +.rails-cache: + cache: + key: "rails-v4" + paths: + - vendor/ruby/ + - vendor/gitaly-ruby/ + policy: pull + .static-analysis-cache: cache: key: "static-analysis-v2" @@ -71,41 +79,41 @@ policy: pull .use-pg11: - image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7.2-golang-1.14-git-2.29-lfs-2.9-chrome-85-node-12.18-yarn-1.22-postgresql-11-graphicsmagick-1.3.34" + image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7.2.patched-golang-1.14-git-2.29-lfs-2.9-chrome-87-node-14.15-yarn-1.22-postgresql-11-graphicsmagick-1.3.34" services: - name: postgres:11.6 command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] - - name: redis:4.0-alpine + - name: redis:5.0-alpine variables: POSTGRES_HOST_AUTH_METHOD: trust .use-pg12: - image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7.2-golang-1.14-git-2.29-lfs-2.9-chrome-85-node-12.18-yarn-1.22-postgresql-12-graphicsmagick-1.3.34" + image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7.2.patched-golang-1.14-git-2.29-lfs-2.9-chrome-87-node-14.15-yarn-1.22-postgresql-12-graphicsmagick-1.3.34" services: - name: postgres:12 command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] - - name: redis:4.0-alpine + - name: redis:5.0-alpine variables: POSTGRES_HOST_AUTH_METHOD: trust .use-pg11-ee: - image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7.2-golang-1.14-git-2.29-lfs-2.9-chrome-85-node-12.18-yarn-1.22-postgresql-11-graphicsmagick-1.3.34" + image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7.2.patched-golang-1.14-git-2.29-lfs-2.9-chrome-87-node-14.15-yarn-1.22-postgresql-11-graphicsmagick-1.3.34" services: - name: postgres:11.6 command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] - - name: redis:4.0-alpine - - name: elasticsearch:7.9.2 + - name: redis:5.0-alpine + - name: elasticsearch:7.10.1 command: ["elasticsearch", "-E", "discovery.type=single-node"] variables: POSTGRES_HOST_AUTH_METHOD: trust .use-pg12-ee: - image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7.2-golang-1.14-git-2.29-lfs-2.9-chrome-85-node-12.18-yarn-1.22-postgresql-12-graphicsmagick-1.3.34" + image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7.2.patched-golang-1.14-git-2.29-lfs-2.9-chrome-87-node-14.15-yarn-1.22-postgresql-12-graphicsmagick-1.3.34" services: - name: postgres:12 command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] - - name: redis:4.0-alpine - - name: elasticsearch:7.9.2 + - name: redis:5.0-alpine + - name: elasticsearch:7.10.1 command: ["elasticsearch", "-E", "discovery.type=single-node"] variables: POSTGRES_HOST_AUTH_METHOD: trust @@ -124,7 +132,7 @@ FOSS_ONLY: '1' .use-docker-in-docker: - image: docker:${DOCKER_VERSION} + image: ${GITLAB_DEPENDENCY_PROXY}docker:${DOCKER_VERSION} services: - docker:${DOCKER_VERSION}-dind variables: diff --git a/.gitlab/ci/notify.gitlab-ci.yml b/.gitlab/ci/notify.gitlab-ci.yml index e18a092bb8..a8c156c7db 100644 --- a/.gitlab/ci/notify.gitlab-ci.yml +++ b/.gitlab/ci/notify.gitlab-ci.yml @@ -1,5 +1,5 @@ .notify-slack: - image: alpine + image: ${GITLAB_DEPENDENCY_PROXY}alpine stage: notify dependencies: [] cache: {} diff --git a/.gitlab/ci/qa.gitlab-ci.yml b/.gitlab/ci/qa.gitlab-ci.yml index 1dc403c9d0..788b482f0a 100644 --- a/.gitlab/ci/qa.gitlab-ci.yml +++ b/.gitlab/ci/qa.gitlab-ci.yml @@ -47,7 +47,7 @@ update-qa-cache: policy: push # We want to rebuild the cache from scratch to ensure stale dependencies are cleaned up. .package-and-qa-base: - image: ruby:2.7-alpine + image: ${GITLAB_DEPENDENCY_PROXY}ruby:2.7-alpine stage: qa retry: 0 script: diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml index a2a16424f4..22aa92779e 100644 --- a/.gitlab/ci/rails.gitlab-ci.yml +++ b/.gitlab/ci/rails.gitlab-ci.yml @@ -141,6 +141,7 @@ setup-test-env: extends: - .rails-job-base + - .setup-test-env-cache - .rails:rules:default-refs-code-backstage-qa - .use-pg11 stage: prepare @@ -180,11 +181,19 @@ setup-test-env: - tmp/tests/second_storage/ when: always -update-rails-cache: +update-setup-test-env-cache: extends: - setup-test-env - .shared:rules:update-cache - artifacts: {} # This job's purpose is only to update the cache. + artifacts: + paths: [] # This job's purpose is only to update the cache. + cache: + policy: push # We want to rebuild the cache from scratch to ensure stale dependencies are cleaned up. + +update-rails-cache: + extends: + - update-setup-test-env-cache + - .rails-cache cache: policy: push # We want to rebuild the cache from scratch to ensure stale dependencies are cleaned up. @@ -376,6 +385,17 @@ db:rollback: - bundle exec rake db:migrate VERSION=20181228175414 - bundle exec rake db:migrate SKIP_SCHEMA_VERSION_CHECK=true +db:gitlabcom-database-testing: + extends: .rails:rules:db:gitlabcom-database-testing + stage: test + image: ruby:2.7-alpine + needs: [] + allow_failure: true + script: + - source scripts/utils.sh + - install_gitlab_gem + - ./scripts/trigger-build gitlab-com-database-testing + gitlab:setup: extends: .db-job-base variables: @@ -433,6 +453,8 @@ rspec:deprecations: variables: SETUP_DB: "false" script: + - grep -h -R "keyword" deprecations/ | awk '{$1=$1};1' | sort | uniq -c | sort + - grep -R "keyword" deprecations/ | wc - run_timed_command "bundle exec rubocop --only Lint/LastKeywordArgument --parallel" artifacts: expire_in: 31d @@ -482,7 +504,6 @@ rspec:feature-flags: - .coverage-base - .rails:rules:rspec-feature-flags stage: post-test - allow_failure: true # We cannot use needs since it would mean needing 84 jobs (since most are parallelized) # so we use `dependencies` here. dependencies: @@ -502,7 +523,11 @@ rspec:feature-flags: - memory-on-boot script: - run_timed_command "bundle install --jobs=$(nproc) --path=vendor --retry=3 --quiet --without default development test production puma unicorn kerberos metrics omnibus ed25519" - - 'run_timed_command "bundle exec scripts/used-feature-flags" || (scripts/slack master-broken "☠️ \`${CI_JOB_NAME}\` failed! ☠️ See ${CI_JOB_URL}" ci_failing "GitLab Bot" && exit 1)' + - if [ "$CI_COMMIT_BRANCH" == "$CI_DEFAULT_BRANCH" ]; then + run_timed_command "bundle exec scripts/used-feature-flags" || (scripts/slack master-broken "☠️ \`${CI_JOB_NAME}\` failed! ☠️ See ${CI_JOB_URL}" ci_failing "GitLab Bot" && exit 1); + else + run_timed_command "bundle exec scripts/used-feature-flags"; + fi # EE/FOSS: default refs (MRs, master, schedules) jobs # ####################################################### diff --git a/.gitlab/ci/releases.gitlab-ci.yml b/.gitlab/ci/releases.gitlab-ci.yml index b3f961afe6..77f23814f3 100644 --- a/.gitlab/ci/releases.gitlab-ci.yml +++ b/.gitlab/ci/releases.gitlab-ci.yml @@ -4,7 +4,7 @@ .merge-train-sync: # We don't need/want any global before/after commands, so we overwrite these # settings. - image: alpine:edge + image: ${GITLAB_DEPENDENCY_PROXY}alpine:edge stage: sync before_script: - apk add --no-cache --update curl bash jq diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml index b7d9f18dcb..c18e898dc1 100644 --- a/.gitlab/ci/review.gitlab-ci.yml +++ b/.gitlab/ci/review.gitlab-ci.yml @@ -25,7 +25,7 @@ review-build-cng: extends: - .default-retry - .review:rules:review-build-cng - image: ruby:2.7-alpine + image: ${GITLAB_DEPENDENCY_PROXY}ruby:2.7-alpine stage: review-prepare before_script: - source ./scripts/utils.sh @@ -199,7 +199,7 @@ review-performance: parallel-spec-reports: extends: - .review:rules:mr-only-manual - image: ruby:2.7-alpine + image: ${GITLAB_DEPENDENCY_PROXY}ruby:2.7-alpine stage: post-qa dependencies: ["review-qa-all"] variables: diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index 5e8cdf0daa..1eafd024f5 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -112,6 +112,7 @@ .workhorse-patterns: &workhorse-patterns - "GITLAB_WORKHORSE_VERSION" - "workhorse/**/*" + - ".gitlab/ci/workhorse.gitlab-ci.yml" .yaml-lint-patterns: &yaml-lint-patterns - ".gitlab-ci.yml" @@ -123,9 +124,12 @@ - ".gitlab/route-map.yml" - "doc/**/*" - ".markdownlint.json" + - "scripts/lint-doc.sh" .frontend-dependency-patterns: &frontend-dependency-patterns - "{package.json,yarn.lock}" + - "config/webpack.config.js" + - "config/helpers/*.js" .frontend-patterns: &frontend-patterns - "{package.json,yarn.lock}" @@ -523,6 +527,13 @@ changes: *db-patterns - <<: *if-merge-request-title-run-all-rspec +.rails:rules:db:gitlabcom-database-testing: + rules: + - if: '$GITLABCOM_DATABASE_TESTING_TRIGGER_TOKEN == null' + when: never + - <<: *if-merge-request + changes: *db-patterns + .rails:rules:ee-and-foss-unit: rules: - changes: *backend-patterns @@ -869,6 +880,7 @@ - <<: *if-not-ee when: never - <<: *if-master-schedule-2-hourly + allow_failure: true - <<: *if-merge-request-title-run-all-rspec .rails:rules:master-schedule-nightly--code-backstage: diff --git a/.gitlab/ci/setup.gitlab-ci.yml b/.gitlab/ci/setup.gitlab-ci.yml index 74510a0a03..27b68115ed 100644 --- a/.gitlab/ci/setup.gitlab-ci.yml +++ b/.gitlab/ci/setup.gitlab-ci.yml @@ -26,7 +26,7 @@ cache gems: dont-interrupt-me: extends: .setup:rules:dont-interrupt-me stage: sync - image: alpine:edge + image: ${GITLAB_DEPENDENCY_PROXY}alpine:edge interruptible: false variables: GIT_STRATEGY: none @@ -52,7 +52,7 @@ no_ee_check: verify-tests-yml: extends: - .setup:rules:verify-tests-yml - image: ruby:2.7-alpine + image: ${GITLAB_DEPENDENCY_PROXY}ruby:2.7-alpine stage: test needs: [] script: @@ -61,7 +61,7 @@ verify-tests-yml: - scripts/verify-tff-mapping .detect-test-base: - image: ruby:2.7 + image: ${GITLAB_DEPENDENCY_PROXY}ruby:2.7 needs: [] stage: prepare script: diff --git a/.gitlab/ci/test-metadata.gitlab-ci.yml b/.gitlab/ci/test-metadata.gitlab-ci.yml index aec0a1640f..b90c02c08e 100644 --- a/.gitlab/ci/test-metadata.gitlab-ci.yml +++ b/.gitlab/ci/test-metadata.gitlab-ci.yml @@ -1,5 +1,5 @@ .tests-metadata-state: - image: ruby:2.7 + image: ${GITLAB_DEPENDENCY_PROXY}ruby:2.7 before_script: - source scripts/utils.sh artifacts: diff --git a/.gitlab/ci/verify-lockfile.gitlab-ci.yml b/.gitlab/ci/verify-lockfile.gitlab-ci.yml new file mode 100644 index 0000000000..6336a428b4 --- /dev/null +++ b/.gitlab/ci/verify-lockfile.gitlab-ci.yml @@ -0,0 +1,11 @@ +verify_lockfile: + stage: test + image: registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7.2-git-2.29-lfs-2.9-node-14.15-yarn-1.22-graphicsmagick-1.3.34 + needs: [] + rules: + - changes: + - yarn.lock + script: + - npm config set @dappelt:registry https://gitlab.com/api/v4/projects/22564149/packages/npm/ + - npx lockfile-lint@4.3.7 --path yarn.lock --allowed-hosts yarn --validate-https + - npx @dappelt/untamper-my-lockfile --lockfile yarn.lock diff --git a/.gitlab/ci/workhorse.gitlab-ci.yml b/.gitlab/ci/workhorse.gitlab-ci.yml index 2913115987..a40eebd131 100644 --- a/.gitlab/ci/workhorse.gitlab-ci.yml +++ b/.gitlab/ci/workhorse.gitlab-ci.yml @@ -1,6 +1,6 @@ workhorse: extends: .workhorse:rules:workhorse - image: golang:1.14 + image: ${GITLAB_DEPENDENCY_PROXY}golang:1.14 stage: test needs: [] script: @@ -8,3 +8,39 @@ workhorse: - git checkout . - scripts/update-workhorse check - make -C workhorse + +workhorse:verify: + extends: .workhorse:rules:workhorse + image: ${GITLAB_DEPENDENCY_PROXY}golang:1.15 + stage: test + needs: [] + script: + - make -C workhorse verify + +.workhorse:test: + extends: .workhorse:rules:workhorse + services: + - name: registry.gitlab.com/gitlab-org/build/cng/gitaly:latest + # Disable the hooks so we don't have to stub the GitLab API + command: ["/usr/bin/env", "GITALY_TESTING_NO_GIT_HOOKS=1", "/scripts/process-wrapper"] + alias: gitaly + variables: + GITALY_ADDRESS: "tcp://gitaly:8075" + stage: test + needs: [] + script: + - go version + - apt-get update && apt-get -y install libimage-exiftool-perl + - make -C workhorse test + +workhorse:test using go 1.13: + extends: .workhorse:test + image: ${GITLAB_DEPENDENCY_PROXY}golang:1.13 + +workhorse:test using go 1.14: + extends: .workhorse:test + image: ${GITLAB_DEPENDENCY_PROXY}golang:1.14 + +workhorse:test using go 1.15: + extends: .workhorse:test + image: ${GITLAB_DEPENDENCY_PROXY}golang:1.15 diff --git a/.gitlab/issue_templates/Actionable Insight.md b/.gitlab/issue_templates/Actionable Insight.md new file mode 100644 index 0000000000..df519f8179 --- /dev/null +++ b/.gitlab/issue_templates/Actionable Insight.md @@ -0,0 +1,28 @@ + + +### Insight + + +### Supporting evidence + + +### Action + + +### Resources + + +- :dove: [Dovetail project](Paste URL for Dovetail project here) +- :mag: [Research issue](Paste URL for research issue here) +- :footprints: [Follow-up issue or epic](Paste URL for follow-up issue or epic here) + +### Tasks +- [ ] Assign this issue to the appropriate Product Manager, Product Designer, or UX Researcher. +- [ ] Add the appropriate `Group` (such as `~"group::source code"`) label to the issue. This helps identify and track actionable insights at the group level. +- [ ] Link this issue back to the original research issue in the GitLab UX Research project and the Dovetail project. + + + + +/label ~"Actionable Insight" + diff --git a/.gitlab/issue_templates/Adoption Engineering.md b/.gitlab/issue_templates/Adoption Engineering.md new file mode 100644 index 0000000000..01e9d0ea03 --- /dev/null +++ b/.gitlab/issue_templates/Adoption Engineering.md @@ -0,0 +1,14 @@ +#Design + + + +#Rollout strategy + + +#Inclusions and exclusions + + +#Segmentation + + +#Tracking diff --git a/.gitlab/issue_templates/Basic Proposal.md b/.gitlab/issue_templates/Basic Proposal.md index 4232561354..8d47e87f8a 100644 --- a/.gitlab/issue_templates/Basic Proposal.md +++ b/.gitlab/issue_templates/Basic Proposal.md @@ -6,6 +6,6 @@ diff --git a/.gitlab/issue_templates/Dogfooding.md b/.gitlab/issue_templates/Dogfooding.md new file mode 100644 index 0000000000..d780fbd3f1 --- /dev/null +++ b/.gitlab/issue_templates/Dogfooding.md @@ -0,0 +1,17 @@ + + +/label ~"dogfooding" ~"group::" ~"section::" ~"Category::" + +## Feature to Dogfood + + +## Goals + + +## Progress Tracker + + +## Why Dogfooding is Important +- https://about.gitlab.com/handbook/values/#dogfooding +- https://about.gitlab.com/handbook/product/product-processes/#dogfood-everything +- https://about.gitlab.com/handbook/engineering/#dogfooding diff --git a/.gitlab/issue_templates/Feature Flag Roll Out.md b/.gitlab/issue_templates/Feature Flag Roll Out.md index 67686b654b..615fb64496 100644 --- a/.gitlab/issue_templates/Feature Flag Roll Out.md +++ b/.gitlab/issue_templates/Feature Flag Roll Out.md @@ -31,7 +31,6 @@ If applicable, any groups/projects that are happy to have this feature turned on ## Roll Out Steps -- [ ] Confirm that QA tests pass with the feature flag enabled (if you're unsure how, contact the relevant [stable counterpart in the Quality department](https://about.gitlab.com/handbook/engineering/quality/#individual-contributors)) - [ ] Enable on staging (`/chatops run feature set feature_name true --staging`) - [ ] Test on staging - [ ] Ensure that documentation has been updated @@ -42,7 +41,7 @@ If applicable, any groups/projects that are happy to have this feature turned on - [ ] Enable on GitLab.com by running chatops command in `#production` (`/chatops run feature set feature_name true`) - [ ] Cross post chatops Slack command to `#support_gitlab-com` ([more guidance when this is necessary in the dev docs](https://docs.gitlab.com/ee/development/feature_flags/controls.html#where-to-run-commands)) and in your team channel - [ ] Announce on the issue that the flag has been enabled -- [ ] Remove feature flag and add changelog entry +- [ ] Remove feature flag and add changelog entry. Ensure that the feature flag definition YAML file has been removed in the **same MR** that is removing the feature flag from the code - [ ] After the flag removal is deployed, [clean up the feature flag](https://docs.gitlab.com/ee/development/feature_flags/controls.html#cleaning-up) by running chatops command in `#production` channel ## Rollback Steps diff --git a/.gitlab/issue_templates/Feature proposal.md b/.gitlab/issue_templates/Feature proposal.md index 66450c37a2..2cdf2341c8 100644 --- a/.gitlab/issue_templates/Feature proposal.md +++ b/.gitlab/issue_templates/Feature proposal.md @@ -61,7 +61,7 @@ Consider adding checkboxes and expectations of users with certain levels of memb ### Availability & Testing @@ -97,7 +97,7 @@ Create tracking issue using the the Snowplow event tracking template. See https: ### What is the type of buyer? +In which enterprise tier should this feature go? See https://about.gitlab.com/handbook/product/pricing/#three-tiers --> ### Is this a cross-stage feature? @@ -111,5 +111,5 @@ Use the following resources to find the appropriate labels: - https://about.gitlab.com/handbook/product/categories/features/ --> /label ~devops:: ~group: ~Category: - +/label ~"GitLab Core"/~"GitLab Premium"/~"GitLab Ultimate" /label ~feature diff --git a/.gitlab/issue_templates/Lean Feature Proposal.md b/.gitlab/issue_templates/Lean Feature Proposal.md index 44210a8902..fb9ac306f3 100644 --- a/.gitlab/issue_templates/Lean Feature Proposal.md +++ b/.gitlab/issue_templates/Lean Feature Proposal.md @@ -14,7 +14,7 @@ -/label ~"feature" ~"group::" ~"section::" ~"Category::" ~"GitLab Core"/~"GitLab Starter"/~"GitLab Premium"/~"GitLab Ultimate" +/label ~"feature" ~"group::" ~"section::" ~"Category::" ~"GitLab Core"/~"GitLab Premium"/~"GitLab Ultimate" + +/label ~"group::database" ~"database::triage" diff --git a/.gitlab/issue_templates/Security developer workflow.md b/.gitlab/issue_templates/Security developer workflow.md index 3de004b031..beb066cdfc 100644 --- a/.gitlab/issue_templates/Security developer workflow.md +++ b/.gitlab/issue_templates/Security developer workflow.md @@ -1,7 +1,7 @@ diff --git a/.gitlab/issue_templates/actionable_insight.md b/.gitlab/issue_templates/actionable_insight.md deleted file mode 100644 index ff6a4f1291..0000000000 --- a/.gitlab/issue_templates/actionable_insight.md +++ /dev/null @@ -1,34 +0,0 @@ -## Actionable Insights -Actionable insights always have a follow-up action that needs to take place as a result of the research observation or data, and a clear recommendation or action associated with it. An actionable insight both defines the insight and clearly calls out the next step. These insights are tracked over time and at the group level. - -#### Link - -- [ ] Provide the link to the Dovetail actionable insight you created earlier (this should contain all the essential details) -- [ ] If applicable, link this actionable insight issue back to the original Research Issue in the GitLab UX Research project - -#### Assign - -- [ ] Assign this issue to the appropriate Product Manager, Product Designer, or UX Researcher - -#### Group label - -- [ ] Add the appropriate `Group` (such as `~"group::source code"`) label to the issue. This is done to identify and track actionable insights at the group level. - -#### Description - -- [ ] Provide some brief details on the actionable insight and the action to take - -------------------------------------------------------------------------------- - -| | PLEASE COMPLETE THE BELOW | -| ------ | ------ | -| Dovetail link: | (URL goes here) | -| Details: | (details go here) | -| Action to take: | (action goes here) | - - - - - - -/label ~"Actionable Insight" diff --git a/.gitlab/merge_request_templates/Change Documentation Location.md b/.gitlab/merge_request_templates/Change Documentation Location.md index 1197c6adc4..0c675d8d0c 100644 --- a/.gitlab/merge_request_templates/Change Documentation Location.md +++ b/.gitlab/merge_request_templates/Change Documentation Location.md @@ -8,9 +8,7 @@ ## Related issues - - -Closes + ## Moving docs to a new location? diff --git a/.gitlab/merge_request_templates/Documentation.md b/.gitlab/merge_request_templates/Documentation.md index 9113bf7d02..0c507277ed 100644 --- a/.gitlab/merge_request_templates/Documentation.md +++ b/.gitlab/merge_request_templates/Documentation.md @@ -11,7 +11,7 @@ ## Related issues - + ## Author's checklist (required) @@ -31,13 +31,13 @@ When applicable: - [ ] Update the [permissions table](https://docs.gitlab.com/ee/user/permissions.html). - [ ] Link docs to and from the higher-level index page, plus other related docs where helpful. -- [ ] Add the [product tier badge](https://docs.gitlab.com/ee/development/documentation/styleguide.html#product-badges) accordingly. -- [ ] Add [GitLab's version history note(s)](https://docs.gitlab.com/ee/development/documentation/styleguide.html#text-for-documentation-requiring-version-text). +- [ ] Add the [product tier badge](https://docs.gitlab.com/ee/development/documentation/styleguide/index.html#product-tier-badges) accordingly. +- [ ] Add [GitLab's version history note(s)](https://docs.gitlab.com/ee/development/documentation/styleguide/index.html#gitlab-versions). - [ ] Add/update the [feature flag section](https://docs.gitlab.com/ee/development/documentation/feature_flags.html). ## Review checklist -All reviewers can help ensure accuracy, clarity, completeness, and adherence to the [Documentation Guidelines](https://docs.gitlab.com/ee/development/documentation/) and [Style Guide](https://docs.gitlab.com/ee/development/documentation/styleguide.html). +All reviewers can help ensure accuracy, clarity, completeness, and adherence to the [Documentation Guidelines](https://docs.gitlab.com/ee/development/documentation/) and [Style Guide](https://docs.gitlab.com/ee/development/documentation/styleguide/). **1. Primary Reviewer** diff --git a/.gitlab/merge_request_templates/New Static Analysis Check.md b/.gitlab/merge_request_templates/New Static Analysis Check.md index 5fd2d31767..66041a784e 100644 --- a/.gitlab/merge_request_templates/New Static Analysis Check.md +++ b/.gitlab/merge_request_templates/New Static Analysis Check.md @@ -1,3 +1,8 @@ + + ## Description of the proposal + + + +
+

{{ __('We want to be sure it is you, please confirm you are not a robot.') }}

+
+ diff --git a/app/assets/javascripts/captcha/init_recaptcha_script.js b/app/assets/javascripts/captcha/init_recaptcha_script.js new file mode 100644 index 0000000000..f546eef7d8 --- /dev/null +++ b/app/assets/javascripts/captcha/init_recaptcha_script.js @@ -0,0 +1,48 @@ +// NOTE: This module will be used in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52044 +import { memoize } from 'lodash'; + +export const RECAPTCHA_API_URL_PREFIX = 'https://www.google.com/recaptcha/api.js'; +export const RECAPTCHA_ONLOAD_CALLBACK_NAME = 'recaptchaOnloadCallback'; + +/** + * Adds the Google reCAPTCHA script tag to the head of the document, and + * returns a promise of the grecaptcha object + * (https://developers.google.com/recaptcha/docs/display#js_api). + * + * It is memoized, so there will only be one instance of the script tag ever + * added to the document. + * + * See the reCAPTCHA documentation for more details: + * + * https://developers.google.com/recaptcha/docs/display#explicit_render + * + */ +export const initRecaptchaScript = memoize(() => { + // Appends the the reCAPTCHA script tag to the head of document + const appendRecaptchaScript = () => { + const script = document.createElement('script'); + script.src = `${RECAPTCHA_API_URL_PREFIX}?onload=${RECAPTCHA_ONLOAD_CALLBACK_NAME}&render=explicit`; + script.classList.add('js-recaptcha-script'); + document.head.appendChild(script); + }; + + return new Promise((resolve) => { + // This global callback resolves the Promise and is passed by name to the reCAPTCHA script. + window[RECAPTCHA_ONLOAD_CALLBACK_NAME] = () => { + // Let's clean up after ourselves. This is also important for testing, because `window` is NOT cleared between tests. + // https://github.com/facebook/jest/issues/1224#issuecomment-444586798. + delete window[RECAPTCHA_ONLOAD_CALLBACK_NAME]; + resolve(window.grecaptcha); + }; + appendRecaptchaScript(); + }); +}); + +/** + * Clears the cached memoization of the default manager. + * + * This is needed for determinism in tests. + */ +export const clearMemoizeCache = () => { + initRecaptchaScript.cache.clear(); +}; diff --git a/app/assets/javascripts/ci_lint/components/ci_lint.vue b/app/assets/javascripts/ci_lint/components/ci_lint.vue index fc47fe8c33..9a55177b15 100644 --- a/app/assets/javascripts/ci_lint/components/ci_lint.vue +++ b/app/assets/javascripts/ci_lint/components/ci_lint.vue @@ -1,8 +1,8 @@ @@ -213,6 +257,8 @@ export default { ref="header" :class="{ 'gl-z-dropdown-menu!': moreActionsShown }" class="js-file-title file-title file-title-flex-parent" + data-qa-selector="file_title_container" + :data-qa-file-name="filePath" @click.self="handleToggleFile" >
@@ -289,6 +335,19 @@ export default { class="file-actions d-flex align-items-center gl-ml-auto gl-align-self-start" > + + + {{ $options.i18n.fileReviewLabel }} + + @@ -340,6 +400,7 @@ export default { ref="ideEditButton" :href="diffFile.ide_edit_path" class="js-ide-edit-blob" + data-qa-selector="edit_in_ide_button" > {{ __('Edit in Web IDE') }} diff --git a/app/assets/javascripts/diffs/components/diff_file_row.vue b/app/assets/javascripts/diffs/components/diff_file_row.vue index 6c5d9170c9..89822ba787 100644 --- a/app/assets/javascripts/diffs/components/diff_file_row.vue +++ b/app/assets/javascripts/diffs/components/diff_file_row.vue @@ -3,9 +3,9 @@ * This component is an iterative step towards refactoring and simplifying `vue_shared/components/file_row.vue` * https://gitlab.com/gitlab-org/gitlab/-/merge_requests/23720 */ -import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; -import FileRow from '~/vue_shared/components/file_row.vue'; import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue'; +import FileRow from '~/vue_shared/components/file_row.vue'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import FileRowStats from './file_row_stats.vue'; export default { diff --git a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue index f62b31734c..1f3ec7092b 100644 --- a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue +++ b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue @@ -1,7 +1,7 @@ + diff --git a/app/assets/javascripts/feature_highlight/index.js b/app/assets/javascripts/feature_highlight/index.js new file mode 100644 index 0000000000..3a8b211b3c --- /dev/null +++ b/app/assets/javascripts/feature_highlight/index.js @@ -0,0 +1,28 @@ +import Vue from 'vue'; + +const init = async () => { + const el = document.querySelector('.js-feature-highlight'); + + if (!el) { + return null; + } + + const { autoDevopsHelpPath, highlight: highlightId, dismissEndpoint } = el.dataset; + const { default: FeatureHighlight } = await import( + /* webpackChunkName: 'feature_highlight' */ './feature_highlight_popover.vue' + ); + + return new Vue({ + el, + render: (h) => + h(FeatureHighlight, { + props: { + autoDevopsHelpPath, + highlightId, + dismissEndpoint, + }, + }), + }); +}; + +export default init; diff --git a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js index 588bd53422..409b4ccbcf 100644 --- a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js +++ b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js @@ -1,12 +1,12 @@ -import DropdownHint from './dropdown_hint'; -import DropdownUser from './dropdown_user'; -import DropdownNonUser from './dropdown_non_user'; -import DropdownEmoji from './dropdown_emoji'; -import NullDropdown from './null_dropdown'; -import DropdownAjaxFilter from './dropdown_ajax_filter'; -import DropdownOperator from './dropdown_operator'; -import DropdownUtils from './dropdown_utils'; import { mergeUrlParams } from '../lib/utils/url_utility'; +import DropdownAjaxFilter from './dropdown_ajax_filter'; +import DropdownEmoji from './dropdown_emoji'; +import DropdownHint from './dropdown_hint'; +import DropdownNonUser from './dropdown_non_user'; +import DropdownOperator from './dropdown_operator'; +import DropdownUser from './dropdown_user'; +import DropdownUtils from './dropdown_utils'; +import NullDropdown from './null_dropdown'; export default class AvailableDropdownMappings { constructor({ diff --git a/app/assets/javascripts/filtered_search/dropdown_ajax_filter.js b/app/assets/javascripts/filtered_search/dropdown_ajax_filter.js index 2c0c3024d3..e317700b09 100644 --- a/app/assets/javascripts/filtered_search/dropdown_ajax_filter.js +++ b/app/assets/javascripts/filtered_search/dropdown_ajax_filter.js @@ -1,9 +1,9 @@ -import { deprecatedCreateFlash as createFlash } from '../flash'; -import AjaxFilter from '../droplab/plugins/ajax_filter'; -import FilteredSearchDropdown from './filtered_search_dropdown'; -import DropdownUtils from './dropdown_utils'; -import FilteredSearchTokenizer from './filtered_search_tokenizer'; import { __ } from '~/locale'; +import AjaxFilter from '../droplab/plugins/ajax_filter'; +import { deprecatedCreateFlash as createFlash } from '../flash'; +import DropdownUtils from './dropdown_utils'; +import FilteredSearchDropdown from './filtered_search_dropdown'; +import FilteredSearchTokenizer from './filtered_search_tokenizer'; export default class DropdownAjaxFilter extends FilteredSearchDropdown { constructor(options = {}) { diff --git a/app/assets/javascripts/filtered_search/dropdown_emoji.js b/app/assets/javascripts/filtered_search/dropdown_emoji.js index 001030b5f5..a22430833a 100644 --- a/app/assets/javascripts/filtered_search/dropdown_emoji.js +++ b/app/assets/javascripts/filtered_search/dropdown_emoji.js @@ -1,9 +1,9 @@ -import { deprecatedCreateFlash as Flash } from '../flash'; +import { __ } from '~/locale'; import Ajax from '../droplab/plugins/ajax'; import Filter from '../droplab/plugins/filter'; -import FilteredSearchDropdown from './filtered_search_dropdown'; +import { deprecatedCreateFlash as Flash } from '../flash'; import DropdownUtils from './dropdown_utils'; -import { __ } from '~/locale'; +import FilteredSearchDropdown from './filtered_search_dropdown'; export default class DropdownEmoji extends FilteredSearchDropdown { constructor(options = {}) { diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js b/app/assets/javascripts/filtered_search/dropdown_hint.js index 1180f8683a..47f350dc6a 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js @@ -1,9 +1,9 @@ import Filter from '~/droplab/plugins/filter'; -import FilteredSearchDropdown from './filtered_search_dropdown'; +import { __ } from '~/locale'; import DropdownUtils from './dropdown_utils'; +import FilteredSearchDropdown from './filtered_search_dropdown'; import FilteredSearchDropdownManager from './filtered_search_dropdown_manager'; import FilteredSearchVisualTokens from './filtered_search_visual_tokens'; -import { __ } from '~/locale'; export default class DropdownHint extends FilteredSearchDropdown { constructor(options = {}) { diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js b/app/assets/javascripts/filtered_search/dropdown_non_user.js index 11261debed..4df1120f16 100644 --- a/app/assets/javascripts/filtered_search/dropdown_non_user.js +++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js @@ -1,9 +1,9 @@ -import { deprecatedCreateFlash as Flash } from '../flash'; +import { __ } from '~/locale'; import Ajax from '../droplab/plugins/ajax'; import Filter from '../droplab/plugins/filter'; -import FilteredSearchDropdown from './filtered_search_dropdown'; +import { deprecatedCreateFlash as Flash } from '../flash'; import DropdownUtils from './dropdown_utils'; -import { __ } from '~/locale'; +import FilteredSearchDropdown from './filtered_search_dropdown'; export default class DropdownNonUser extends FilteredSearchDropdown { constructor(options = {}) { diff --git a/app/assets/javascripts/filtered_search/dropdown_operator.js b/app/assets/javascripts/filtered_search/dropdown_operator.js index 8fee3385de..0da8cd0ad8 100644 --- a/app/assets/javascripts/filtered_search/dropdown_operator.js +++ b/app/assets/javascripts/filtered_search/dropdown_operator.js @@ -1,7 +1,7 @@ import Filter from '~/droplab/plugins/filter'; import { __ } from '~/locale'; -import FilteredSearchDropdown from './filtered_search_dropdown'; import DropdownUtils from './dropdown_utils'; +import FilteredSearchDropdown from './filtered_search_dropdown'; import FilteredSearchDropdownManager from './filtered_search_dropdown_manager'; import FilteredSearchVisualTokens from './filtered_search_visual_tokens'; diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js b/app/assets/javascripts/filtered_search/dropdown_utils.js index 22c98f360e..c98d1f8e06 100644 --- a/app/assets/javascripts/filtered_search/dropdown_utils.js +++ b/app/assets/javascripts/filtered_search/dropdown_utils.js @@ -1,7 +1,7 @@ import { last } from 'lodash'; import FilteredSearchContainer from './container'; -import FilteredSearchTokenizer from './filtered_search_tokenizer'; import FilteredSearchDropdownManager from './filtered_search_dropdown_manager'; +import FilteredSearchTokenizer from './filtered_search_tokenizer'; import FilteredSearchVisualTokens from './filtered_search_visual_tokens'; export default class DropdownUtils { diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js index 7434cc4c5d..fcc7caa9ff 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js @@ -1,7 +1,7 @@ +import { FILTER_TYPE } from './constants'; import DropdownUtils from './dropdown_utils'; import FilteredSearchDropdownManager from './filtered_search_dropdown_manager'; import FilteredSearchVisualTokens from './filtered_search_visual_tokens'; -import { FILTER_TYPE } from './constants'; const DATA_DROPDOWN_TRIGGER = 'data-dropdown-trigger'; diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js index 3c630c26bc..ebaa3ef98b 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js @@ -1,11 +1,11 @@ import { last } from 'lodash'; import AvailableDropdownMappings from 'ee_else_ce/filtered_search/available_dropdown_mappings'; import DropLab from '~/droplab/drop_lab'; -import FilteredSearchContainer from './container'; -import FilteredSearchTokenKeys from './filtered_search_token_keys'; -import DropdownUtils from './dropdown_utils'; -import FilteredSearchVisualTokens from './filtered_search_visual_tokens'; import { DROPDOWN_TYPE } from './constants'; +import FilteredSearchContainer from './container'; +import DropdownUtils from './dropdown_utils'; +import FilteredSearchTokenKeys from './filtered_search_token_keys'; +import FilteredSearchVisualTokens from './filtered_search_visual_tokens'; export default class FilteredSearchDropdownManager { constructor({ diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index 11b2eb839c..69d19074cd 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -1,19 +1,7 @@ import { last } from 'lodash'; import recentSearchesStorageKeys from 'ee_else_ce/filtered_search/recent_searches_storage_keys'; -import { getParameterByName, getUrlParamsArray } from '~/lib/utils/common_utils'; import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys'; -import { visitUrl } from '../lib/utils/url_utility'; -import { deprecatedCreateFlash as Flash } from '../flash'; -import FilteredSearchContainer from './container'; -import RecentSearchesRoot from './recent_searches_root'; -import RecentSearchesStore from './stores/recent_searches_store'; -import RecentSearchesService from './services/recent_searches_service'; -import eventHub from './event_hub'; -import { addClassIfElementExists } from '../lib/utils/dom_utils'; -import FilteredSearchTokenizer from './filtered_search_tokenizer'; -import FilteredSearchDropdownManager from './filtered_search_dropdown_manager'; -import FilteredSearchVisualTokens from './filtered_search_visual_tokens'; -import DropdownUtils from './dropdown_utils'; +import { getParameterByName, getUrlParamsArray } from '~/lib/utils/common_utils'; import { ENTER_KEY_CODE, BACKSPACE_KEY_CODE, @@ -22,6 +10,18 @@ import { DOWN_KEY_CODE, } from '~/lib/utils/keycodes'; import { __ } from '~/locale'; +import { deprecatedCreateFlash as Flash } from '../flash'; +import { addClassIfElementExists } from '../lib/utils/dom_utils'; +import { visitUrl } from '../lib/utils/url_utility'; +import FilteredSearchContainer from './container'; +import DropdownUtils from './dropdown_utils'; +import eventHub from './event_hub'; +import FilteredSearchDropdownManager from './filtered_search_dropdown_manager'; +import FilteredSearchTokenizer from './filtered_search_tokenizer'; +import FilteredSearchVisualTokens from './filtered_search_visual_tokens'; +import RecentSearchesRoot from './recent_searches_root'; +import RecentSearchesService from './services/recent_searches_service'; +import RecentSearchesStore from './stores/recent_searches_store'; export default class FilteredSearchManager { constructor({ diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js index 4e594dfa91..eec4db41b0 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js +++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js @@ -1,6 +1,6 @@ -import VisualTokenValue from './visual_token_value'; import { objectToQueryString, spriteIcon } from '~/lib/utils/common_utils'; import FilteredSearchContainer from './container'; +import VisualTokenValue from './visual_token_value'; export default class FilteredSearchVisualTokens { static permissibleOperatorValues = ['=', '!=']; diff --git a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js index 46867b184c..2c58506985 100644 --- a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js +++ b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js @@ -1,6 +1,6 @@ import { flattenDeep } from 'lodash'; -import FilteredSearchTokenKeys from './filtered_search_token_keys'; import { __ } from '~/locale'; +import FilteredSearchTokenKeys from './filtered_search_token_keys'; export const tokenKeys = [ { diff --git a/app/assets/javascripts/filtered_search/recent_searches_root.js b/app/assets/javascripts/filtered_search/recent_searches_root.js index 6c8e77a7fe..1182cb3421 100644 --- a/app/assets/javascripts/filtered_search/recent_searches_root.js +++ b/app/assets/javascripts/filtered_search/recent_searches_root.js @@ -28,19 +28,18 @@ class RecentSearchesRoot { const { state } = this.store; this.vm = new Vue({ el: this.wrapperElement, - components: { - RecentSearchesDropdownContent, - }, data() { return state; }, - template: ` - - `, + render(h) { + return h(RecentSearchesDropdownContent, { + props: { + items: this.recentSearches, + isLocalStorageAvailable: this.isLocalStorageAvailable, + allowedKeys: this.allowedKeys, + }, + }); + }, }); } diff --git a/app/assets/javascripts/filtered_search/recent_searches_storage_keys.js b/app/assets/javascripts/filtered_search/recent_searches_storage_keys.js index 54d49821d9..446a0e5eb2 100644 --- a/app/assets/javascripts/filtered_search/recent_searches_storage_keys.js +++ b/app/assets/javascripts/filtered_search/recent_searches_storage_keys.js @@ -3,4 +3,6 @@ export default { merge_requests: 'merge-request-recent-searches', group_members: 'group-members-recent-searches', group_invited_members: 'group-invited-members-recent-searches', + project_members: 'project-members-recent-searches', + project_group_links: 'project-group-links-recent-searches', }; diff --git a/app/assets/javascripts/filtered_search/services/recent_searches_service.js b/app/assets/javascripts/filtered_search/services/recent_searches_service.js index a056dea928..56824977a4 100644 --- a/app/assets/javascripts/filtered_search/services/recent_searches_service.js +++ b/app/assets/javascripts/filtered_search/services/recent_searches_service.js @@ -1,5 +1,5 @@ -import RecentSearchesServiceError from './recent_searches_service_error'; import AccessorUtilities from '../../lib/utils/accessor'; +import RecentSearchesServiceError from './recent_searches_service_error'; class RecentSearchesService { constructor(localStorageKey = 'issuable-recent-searches') { diff --git a/app/assets/javascripts/filtered_search/visual_token_value.js b/app/assets/javascripts/filtered_search/visual_token_value.js index 0d36126943..7f4445ad4c 100644 --- a/app/assets/javascripts/filtered_search/visual_token_value.js +++ b/app/assets/javascripts/filtered_search/visual_token_value.js @@ -1,13 +1,13 @@ import { escape } from 'lodash'; import { USER_TOKEN_TYPES } from 'ee_else_ce/filtered_search/constants'; +import * as Emoji from '~/emoji'; import FilteredSearchContainer from '~/filtered_search/container'; -import FilteredSearchVisualTokens from '~/filtered_search/filtered_search_visual_tokens'; -import AjaxCache from '~/lib/utils/ajax_cache'; import DropdownUtils from '~/filtered_search/dropdown_utils'; +import FilteredSearchVisualTokens from '~/filtered_search/filtered_search_visual_tokens'; import { deprecatedCreateFlash as Flash } from '~/flash'; +import AjaxCache from '~/lib/utils/ajax_cache'; import UsersCache from '~/lib/utils/users_cache'; import { __ } from '~/locale'; -import * as Emoji from '~/emoji'; export default class VisualTokenValue { constructor(tokenValue, tokenType, tokenOperator) { diff --git a/app/assets/javascripts/frequent_items/components/app.vue b/app/assets/javascripts/frequent_items/components/app.vue index 68cc864581..69f89aa385 100644 --- a/app/assets/javascripts/frequent_items/components/app.vue +++ b/app/assets/javascripts/frequent_items/components/app.vue @@ -1,13 +1,13 @@ diff --git a/app/assets/javascripts/frequent_items/index.js b/app/assets/javascripts/frequent_items/index.js index cef8be37a4..eb8a404e8a 100644 --- a/app/assets/javascripts/frequent_items/index.js +++ b/app/assets/javascripts/frequent_items/index.js @@ -1,8 +1,8 @@ import $ from 'jquery'; import Vue from 'vue'; +import { createStore } from '~/frequent_items/store'; import Translate from '~/vue_shared/translate'; import eventHub from './event_hub'; -import { createStore } from '~/frequent_items/store'; Vue.use(Translate); diff --git a/app/assets/javascripts/frequent_items/store/actions.js b/app/assets/javascripts/frequent_items/store/actions.js index f415648762..90b454d1b4 100644 --- a/app/assets/javascripts/frequent_items/store/actions.js +++ b/app/assets/javascripts/frequent_items/store/actions.js @@ -1,7 +1,7 @@ import AccessorUtilities from '~/lib/utils/accessor'; -import * as types from './mutation_types'; -import { getTopFrequentItems } from '../utils'; import { getGroups, getProjects } from '~/rest_api'; +import { getTopFrequentItems } from '../utils'; +import * as types from './mutation_types'; export const setNamespace = ({ commit }, namespace) => { commit(types.SET_NAMESPACE, namespace); diff --git a/app/assets/javascripts/frequent_items/utils.js b/app/assets/javascripts/frequent_items/utils.js index 63fe0ef20b..88519d934c 100644 --- a/app/assets/javascripts/frequent_items/utils.js +++ b/app/assets/javascripts/frequent_items/utils.js @@ -1,5 +1,5 @@ -import { take } from 'lodash'; import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils'; +import { take } from 'lodash'; import { sanitize } from '~/lib/dompurify'; import { FREQUENT_ITEMS, HOUR_IN_MS } from './constants'; diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index cf9ff87f25..d209a971c3 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -1,19 +1,23 @@ import $ from 'jquery'; import '~/lib/utils/jquery_at_who'; import { escape, template } from 'lodash'; -import { s__ } from '~/locale'; -import SidebarMediator from '~/sidebar/sidebar_mediator'; -import { isUserBusy } from '~/set_status_modal/utils'; -import glRegexp from './lib/utils/regexp'; -import AjaxCache from './lib/utils/ajax_cache'; -import axios from '~/lib/utils/axios_utils'; -import { spriteIcon } from './lib/utils/common_utils'; import * as Emoji from '~/emoji'; +import axios from '~/lib/utils/axios_utils'; +import { s__ } from '~/locale'; +import { isUserBusy } from '~/set_status_modal/utils'; +import SidebarMediator from '~/sidebar/sidebar_mediator'; +import AjaxCache from './lib/utils/ajax_cache'; +import { spriteIcon } from './lib/utils/common_utils'; +import glRegexp from './lib/utils/regexp'; function sanitize(str) { return str.replace(/<(?:.|\n)*?>/gm, ''); } +function createMemberSearchString(member) { + return `${member.name.replace(/ /g, '')} ${member.username}`; +} + export function membersBeforeSave(members) { return members.map((member) => { const GROUP_TYPE = 'Group'; @@ -40,7 +44,7 @@ export function membersBeforeSave(members) { username: member.username, avatarTag: autoCompleteAvatar.length === 1 ? txtAvatar : imgAvatar, title: sanitize(title), - search: sanitize(`${member.username} ${member.name}`), + search: sanitize(createMemberSearchString(member)), icon: avatarIcon, availability: member?.availability, }; @@ -186,59 +190,43 @@ class GfmAutoComplete { } setupEmoji($input) { - const self = this; - const { filter, ...defaults } = this.getDefaultCallbacks(); + const fetchData = this.fetchData.bind(this); // Emoji $input.atwho({ at: ':', - displayTpl(value) { - let tmpl = GfmAutoComplete.Loading.template; - if (value && value.name) { - tmpl = GfmAutoComplete.Emoji.templateFunction(value.name); - } - return tmpl; - }, + displayTpl: GfmAutoComplete.Emoji.templateFunction, insertTpl: GfmAutoComplete.Emoji.insertTemplateFunction, skipSpecialCharacterTest: true, data: GfmAutoComplete.defaultLoadingData, callbacks: { - ...defaults, + ...this.getDefaultCallbacks(), matcher(flag, subtext) { const regexp = new RegExp(`(?:[^${glRegexp.unicodeLetters}0-9:]|\n|^):([^:]*)$`, 'gi'); const match = regexp.exec(subtext); return match && match.length ? match[1] : null; }, - filter(query, items, searchKey) { - const filtered = filter.call(this, query, items, searchKey); - if (query.length === 0 || GfmAutoComplete.isLoading(items)) { - return filtered; + filter(query, items) { + if (GfmAutoComplete.isLoading(items)) { + fetchData(this.$inputor, this.at); + return items; } - // map from value to " is of ", arranged by emoji - const emojis = {}; - filtered.forEach(({ name: value }) => { - self.emojiLookup[value].forEach(({ emoji: { name }, kind }) => { - let entry = emojis[name]; - if (!entry) { - entry = {}; - emojis[name] = entry; - } - if (!(kind in entry) || value.localeCompare(entry[kind]) < 0) { - entry[kind] = value; - } - }); - }); + return GfmAutoComplete.Emoji.filter(query); + }, + sorter(query, items) { + this.setting.highlightFirst = this.setting.alwaysHighlightFirst || query.length > 0; + if (GfmAutoComplete.isLoading(items)) { + this.setting.highlightFirst = false; + return items; + } - // collate results to list, prefering name > unicode > alias > description - const results = []; - Object.values(emojis).forEach(({ name, unicode, alias, description }) => { - results.push(name || unicode || alias || description); - }); + if (query.length === 0) { + return items; + } - // return to the form atwho wants - return results.map((name) => ({ name })); + return GfmAutoComplete.Emoji.sorter(items); }, }, }); @@ -298,9 +286,7 @@ class GfmAutoComplete { // Cache assignees list for easier filtering later assignees = - SidebarMediator.singleton?.store?.assignees?.map( - (assignee) => `${assignee.username} ${assignee.name}`, - ) || []; + SidebarMediator.singleton?.store?.assignees?.map(createMemberSearchString) || []; const match = GfmAutoComplete.defaultMatcher(flag, subtext, this.app.controllers); return match && match.length ? match[1] : null; @@ -672,32 +658,7 @@ class GfmAutoComplete { async loadEmojiData($input, at) { await Emoji.initEmojiMap(); - // All the emoji - const emojis = Emoji.getAllEmoji(); - - // Add all of the fields to atwho's database - this.loadData($input, at, [ - ...Object.keys(emojis), // Names - ...Object.values(emojis).flatMap(({ aliases }) => aliases), // Aliases - ...Object.values(emojis).map(({ e }) => e), // Unicode values - ...Object.values(emojis).map(({ d }) => d), // Descriptions - ]); - - // Construct a lookup that can correlate a value to " is the of " - const lookup = {}; - const add = (key, kind, emoji) => { - if (!(key in lookup)) { - lookup[key] = []; - } - lookup[key].push({ kind, emoji }); - }; - Object.values(emojis).forEach((emoji) => { - add(emoji.name, 'name', emoji); - add(emoji.d, 'description', emoji); - add(emoji.e, 'unicode', emoji); - emoji.aliases.forEach((a) => add(a, 'alias', emoji)); - }); - this.emojiLookup = lookup; + this.loadData($input, at, ['loaded']); GfmAutoComplete.glEmojiTag = Emoji.glEmojiTag; } @@ -770,36 +731,38 @@ GfmAutoComplete.typesWithBackendFiltering = ['vulnerabilities']; GfmAutoComplete.isTypeWithBackendFiltering = (type) => GfmAutoComplete.typesWithBackendFiltering.includes(GfmAutoComplete.atTypeMap[type]); -function findEmoji(name) { - return Emoji.searchEmoji(name, { match: 'contains', raw: true }).sort((a, b) => { - if (a.index !== b.index) { - return a.index - b.index; - } - return a.field.localeCompare(b.field); - }); -} - // Emoji GfmAutoComplete.glEmojiTag = null; GfmAutoComplete.Emoji = { insertTemplateFunction(value) { - const results = findEmoji(value.name); - if (results.length) { - return `:${results[0].emoji.name}:`; - } - return `:${value.name}:`; + return `:${value.emoji.name}:`; }, - templateFunction(name) { - // glEmojiTag helper is loaded on-demand in fetchData() - if (!GfmAutoComplete.glEmojiTag) return `
  • ${name}
  • `; - - const results = findEmoji(name); - if (!results.length) { - return `
  • ${name} ${GfmAutoComplete.glEmojiTag(name)}
  • `; + templateFunction(item) { + if (GfmAutoComplete.isLoading(item)) { + return GfmAutoComplete.Loading.template; } - const { field, emoji } = results[0]; - return `
  • ${field} ${GfmAutoComplete.glEmojiTag(emoji.name)}
  • `; + const escapedFieldValue = escape(item.fieldValue); + if (!GfmAutoComplete.glEmojiTag) { + return `
  • ${escapedFieldValue}
  • `; + } + + return `
  • ${escapedFieldValue} ${GfmAutoComplete.glEmojiTag(item.emoji.name)}
  • `; + }, + filter(query) { + if (query.length === 0) { + return Object.values(Emoji.getAllEmoji()) + .map((emoji) => ({ + emoji, + fieldValue: emoji.name, + })) + .slice(0, 20); + } + + return Emoji.searchEmoji(query); + }, + sorter(items) { + return Emoji.sortEmoji(items); }, }; // Team Members diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js index 3e777c2dc0..3a04779e48 100644 --- a/app/assets/javascripts/gl_form.js +++ b/app/assets/javascripts/gl_form.js @@ -1,9 +1,9 @@ -import $ from 'jquery'; import autosize from 'autosize'; +import $ from 'jquery'; import GfmAutoComplete, { defaultAutocompleteConfig } from 'ee_else_ce/gfm_auto_complete'; +import { disableButtonIfEmptyField } from '~/lib/utils/common_utils'; import dropzoneInput from './dropzone_input'; import { addMarkdownListeners, removeMarkdownListeners } from './lib/utils/text_markdown'; -import { disableButtonIfEmptyField } from '~/lib/utils/common_utils'; export default class GLForm { /** diff --git a/app/assets/javascripts/gpg_badges.js b/app/assets/javascripts/gpg_badges.js index 3a8ae56bb8..cde2cd6d6a 100644 --- a/app/assets/javascripts/gpg_badges.js +++ b/app/assets/javascripts/gpg_badges.js @@ -1,7 +1,7 @@ import $ from 'jquery'; -import { parseQueryStringIntoObject } from '~/lib/utils/common_utils'; -import axios from '~/lib/utils/axios_utils'; import { deprecatedCreateFlash as createFlash } from '~/flash'; +import axios from '~/lib/utils/axios_utils'; +import { parseQueryStringIntoObject } from '~/lib/utils/common_utils'; import { __ } from '~/locale'; export default class GpgBadges { diff --git a/app/assets/javascripts/grafana_integration/components/grafana_integration.vue b/app/assets/javascripts/grafana_integration/components/grafana_integration.vue index 7a991ac245..7b029c6cf5 100644 --- a/app/assets/javascripts/grafana_integration/components/grafana_integration.vue +++ b/app/assets/javascripts/grafana_integration/components/grafana_integration.vue @@ -54,9 +54,9 @@ export default {
    diff --git a/app/assets/javascripts/pages/projects/forks/new/index.js b/app/assets/javascripts/pages/projects/forks/new/index.js index 7948585973..a018d7e092 100644 --- a/app/assets/javascripts/pages/projects/forks/new/index.js +++ b/app/assets/javascripts/pages/projects/forks/new/index.js @@ -1,13 +1,10 @@ import Vue from 'vue'; -import { parseBoolean } from '~/lib/utils/common_utils'; import ForkGroupsList from './components/fork_groups_list.vue'; document.addEventListener('DOMContentLoaded', () => { const mountElement = document.getElementById('fork-groups-mount-element'); - const { endpoint, canCreateProject } = mountElement.dataset; - - const hasReachedProjectLimit = !parseBoolean(canCreateProject); + const { endpoint } = mountElement.dataset; return new Vue({ el: mountElement, @@ -15,7 +12,6 @@ document.addEventListener('DOMContentLoaded', () => { return h(ForkGroupsList, { props: { endpoint, - hasReachedProjectLimit, }, }); }, diff --git a/app/assets/javascripts/pages/projects/incidents/show/index.js b/app/assets/javascripts/pages/projects/incidents/show/index.js index 5b3f03cd57..a75b68873e 100644 --- a/app/assets/javascripts/pages/projects/incidents/show/index.js +++ b/app/assets/javascripts/pages/projects/incidents/show/index.js @@ -1,9 +1,7 @@ -import initSidebarBundle from '~/sidebar/sidebar_bundle'; import initRelatedIssues from '~/related_issues'; +import initSidebarBundle from '~/sidebar/sidebar_bundle'; import initShow from '../../issues/show'; initShow(); -if (!gon.features?.vueIssuableSidebar) { - initSidebarBundle(); -} +initSidebarBundle(); initRelatedIssues(); diff --git a/app/assets/javascripts/pages/projects/index.js b/app/assets/javascripts/pages/projects/index.js index 3e9962a4e7..45e9643b3f 100644 --- a/app/assets/javascripts/pages/projects/index.js +++ b/app/assets/javascripts/pages/projects/index.js @@ -1,5 +1,5 @@ -import Project from './project'; import ShortcutsNavigation from '../../behaviors/shortcuts/shortcuts_navigation'; +import Project from './project'; new Project(); // eslint-disable-line no-new new ShortcutsNavigation(); // eslint-disable-line no-new diff --git a/app/assets/javascripts/pages/projects/init_blob.js b/app/assets/javascripts/pages/projects/init_blob.js index 5eb0d32326..06aba866cc 100644 --- a/app/assets/javascripts/pages/projects/init_blob.js +++ b/app/assets/javascripts/pages/projects/init_blob.js @@ -1,9 +1,9 @@ -import LineHighlighter from '~/line_highlighter'; -import BlobLinePermalinkUpdater from '~/blob/blob_line_permalink_updater'; -import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; import ShortcutsBlob from '~/behaviors/shortcuts/shortcuts_blob'; +import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; import BlobForkSuggestion from '~/blob/blob_fork_suggestion'; +import BlobLinePermalinkUpdater from '~/blob/blob_line_permalink_updater'; import initBlobBundle from '~/blob_edit/blob_bundle'; +import LineHighlighter from '~/line_highlighter'; export default () => { new LineHighlighter(); // eslint-disable-line no-new diff --git a/app/assets/javascripts/pages/projects/init_form.js b/app/assets/javascripts/pages/projects/init_form.js index 9f20a3e4e4..764c23e9a9 100644 --- a/app/assets/javascripts/pages/projects/init_form.js +++ b/app/assets/javascripts/pages/projects/init_form.js @@ -1,7 +1,7 @@ -import ZenMode from '~/zen_mode'; import GLForm from '~/gl_form'; +import ZenMode from '~/zen_mode'; -export default function ($formEl) { +export default function initProjectForm($formEl) { new ZenMode(); // eslint-disable-line no-new new GLForm($formEl); // eslint-disable-line no-new } diff --git a/app/assets/javascripts/pages/projects/issues/form.js b/app/assets/javascripts/pages/projects/issues/form.js index 34c7ee2e60..4e35f28ab0 100644 --- a/app/assets/javascripts/pages/projects/issues/form.js +++ b/app/assets/javascripts/pages/projects/issues/form.js @@ -2,12 +2,12 @@ import $ from 'jquery'; import IssuableForm from 'ee_else_ce/issuable_form'; +import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; import GLForm from '~/gl_form'; +import initSuggestions from '~/issuable_suggestions'; import LabelsSelect from '~/labels_select'; import MilestoneSelect from '~/milestone_select'; -import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; import IssuableTemplateSelectors from '~/templates/issuable_template_selectors'; -import initSuggestions from '~/issuable_suggestions'; export default () => { new ShortcutsNavigation(); diff --git a/app/assets/javascripts/pages/projects/issues/index/index.js b/app/assets/javascripts/pages/projects/issues/index/index.js index f3ccedc47c..525d90e162 100644 --- a/app/assets/javascripts/pages/projects/issues/index/index.js +++ b/app/assets/javascripts/pages/projects/issues/index/index.js @@ -1,15 +1,15 @@ /* eslint-disable no-new */ import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys'; -import IssuableIndex from '~/issuable_index'; import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; -import UsersSelect from '~/users_select'; -import initFilteredSearch from '~/pages/search/init_filtered_search'; -import { FILTERED_SEARCH } from '~/pages/constants'; -import { ISSUABLE_INDEX } from '~/pages/projects/constants'; +import initIssuableByEmail from '~/issuable/init_issuable_by_email'; +import IssuableIndex from '~/issuable_index'; import initIssuablesList from '~/issues_list'; import initManualOrdering from '~/manual_ordering'; -import { showLearnGitLabIssuesPopover } from '~/onboarding_issues'; +import { FILTERED_SEARCH } from '~/pages/constants'; +import { ISSUABLE_INDEX } from '~/pages/projects/constants'; +import initFilteredSearch from '~/pages/search/init_filtered_search'; +import UsersSelect from '~/users_select'; IssuableFilteredSearchTokenKeys.addExtraTokensForIssues(); @@ -25,4 +25,4 @@ new UsersSelect(); initManualOrdering(); initIssuablesList(); -showLearnGitLabIssuesPopover(); +initIssuableByEmail(); diff --git a/app/assets/javascripts/pages/projects/issues/service_desk/filtered_search.js b/app/assets/javascripts/pages/projects/issues/service_desk/filtered_search.js index ccb453a59e..bec207aa43 100644 --- a/app/assets/javascripts/pages/projects/issues/service_desk/filtered_search.js +++ b/app/assets/javascripts/pages/projects/issues/service_desk/filtered_search.js @@ -1,6 +1,6 @@ /* eslint-disable class-methods-use-this */ -import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys'; import FilteredSearchManager from 'ee_else_ce/filtered_search/filtered_search_manager'; +import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys'; const AUTHOR_PARAM_KEY = 'author_username'; diff --git a/app/assets/javascripts/pages/projects/issues/service_desk/index.js b/app/assets/javascripts/pages/projects/issues/service_desk/index.js index 231ee6732e..5be9f6117d 100644 --- a/app/assets/javascripts/pages/projects/issues/service_desk/index.js +++ b/app/assets/javascripts/pages/projects/issues/service_desk/index.js @@ -1,5 +1,5 @@ -import FilteredSearchServiceDesk from './filtered_search'; import initIssuablesList from '~/issues_list'; +import FilteredSearchServiceDesk from './filtered_search'; const supportBotData = JSON.parse( document.querySelector('.js-service-desk-issues').dataset.supportBot, diff --git a/app/assets/javascripts/pages/projects/issues/show.js b/app/assets/javascripts/pages/projects/issues/show.js index 7068574ecb..992bf3c54f 100644 --- a/app/assets/javascripts/pages/projects/issues/show.js +++ b/app/assets/javascripts/pages/projects/issues/show.js @@ -1,22 +1,21 @@ import loadAwardsHandler from '~/awards_handler'; -import initIssuableSidebar from '~/init_issuable_sidebar'; -import Issue from '~/issue'; import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable'; -import ZenMode from '~/zen_mode'; -import '~/notes/index'; -import { store } from '~/notes/stores'; -import { initIssuableApp, initIssueHeaderActions } from '~/issue_show/issue'; -import initIncidentApp from '~/issue_show/incident'; -import initIssuableHeaderWarning from '~/vue_shared/components/issuable/init_issuable_header_warning'; -import initSentryErrorStackTraceApp from '~/sentry_error_stack_trace'; -import initRelatedMergeRequestsApp from '~/related_merge_requests'; -import { parseIssuableData } from '~/issue_show/utils/parse_data'; -import initInviteMemberTrigger from '~/invite_member/init_invite_member_trigger'; +import initIssuableSidebar from '~/init_issuable_sidebar'; import initInviteMemberModal from '~/invite_member/init_invite_member_modal'; - +import initInviteMemberTrigger from '~/invite_member/init_invite_member_trigger'; import { IssuableType } from '~/issuable_show/constants'; +import Issue from '~/issue'; +import '~/notes/index'; +import initIncidentApp from '~/issue_show/incident'; +import { initIssuableApp, initIssueHeaderActions } from '~/issue_show/issue'; +import { parseIssuableData } from '~/issue_show/utils/parse_data'; +import { store } from '~/notes/stores'; +import initRelatedMergeRequestsApp from '~/related_merge_requests'; +import initSentryErrorStackTraceApp from '~/sentry_error_stack_trace'; +import initIssuableHeaderWarning from '~/vue_shared/components/issuable/init_issuable_header_warning'; +import ZenMode from '~/zen_mode'; -export default function () { +export default function initShowIssue() { const initialDataEl = document.getElementById('js-issuable-app'); const { issueType, ...issuableData } = parseIssuableData(initialDataEl); diff --git a/app/assets/javascripts/pages/projects/issues/show/index.js b/app/assets/javascripts/pages/projects/issues/show/index.js index 630add51a9..e4f99d1e7f 100644 --- a/app/assets/javascripts/pages/projects/issues/show/index.js +++ b/app/assets/javascripts/pages/projects/issues/show/index.js @@ -1,9 +1,7 @@ -import initSidebarBundle from '~/sidebar/sidebar_bundle'; import initRelatedIssues from '~/related_issues'; +import initSidebarBundle from '~/sidebar/sidebar_bundle'; import initShow from '../show'; initShow(); -if (gon.features && !gon.features.vueIssuableSidebar) { - initSidebarBundle(); -} +initSidebarBundle(); initRelatedIssues(); diff --git a/app/assets/javascripts/pages/projects/jobs/index/index.js b/app/assets/javascripts/pages/projects/jobs/index/index.js index c343a37b29..6a70d4cf26 100644 --- a/app/assets/javascripts/pages/projects/jobs/index/index.js +++ b/app/assets/javascripts/pages/projects/jobs/index/index.js @@ -1,26 +1,19 @@ import Vue from 'vue'; import GlCountdown from '~/vue_shared/components/gl_countdown.vue'; -import Tracking from '~/tracking'; document.addEventListener('DOMContentLoaded', () => { const remainingTimeElements = document.querySelectorAll('.js-remaining-time'); remainingTimeElements.forEach( (el) => new Vue({ - ...GlCountdown, el, - propsData: { - endDateString: el.dateTime, + render(h) { + return h(GlCountdown, { + props: { + endDateString: el.dateTime, + }, + }); }, }), ); - - const trackButtonClick = () => { - if (gon.tracking_data) { - const { category, action, ...data } = gon.tracking_data; - Tracking.event(category, action, data); - } - }; - const buttons = document.querySelectorAll('.js-empty-state-button'); - buttons.forEach((button) => button.addEventListener('click', trackButtonClick)); }); diff --git a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue index 8626fd1823..81ffaa6f7a 100644 --- a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue +++ b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue @@ -1,9 +1,9 @@ + diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_b.vue b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_b.vue new file mode 100644 index 0000000000..0393793bfe --- /dev/null +++ b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_b.vue @@ -0,0 +1,27 @@ + + diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/constants/index.js b/app/assets/javascripts/pages/projects/learn_gitlab/constants/index.js new file mode 100644 index 0000000000..8606af2978 --- /dev/null +++ b/app/assets/javascripts/pages/projects/learn_gitlab/constants/index.js @@ -0,0 +1,12 @@ +import { s__ } from '~/locale'; + +export const ACTION_TEXT = { + gitWrite: s__('LearnGitLab|Create a repository'), + userAdded: s__('LearnGitLab|Invite your colleagues'), + pipelineCreated: s__('LearnGitLab|Set-up CI/CD'), + trialStarted: s__('LearnGitLab|Start a free trial of GitLab Gold'), + codeOwnersEnabled: s__('LearnGitLab|Add code owners'), + requiredMrApprovalsEnabled: s__('LearnGitLab|Enable require merge approvals'), + mergeRequestCreated: s__('LearnGitLab|Submit a merge request (MR)'), + securityScanEnabled: s__('LearnGitLab|Run a Security scan using CI/CD'), +}; diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js b/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js new file mode 100644 index 0000000000..c4dec89b98 --- /dev/null +++ b/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js @@ -0,0 +1,25 @@ +import Vue from 'vue'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; +import LearnGitlabA from '../components/learn_gitlab_a.vue'; +import LearnGitlabB from '../components/learn_gitlab_b.vue'; + +function initLearnGitlab() { + const el = document.getElementById('js-learn-gitlab-app'); + + if (!el) { + return false; + } + + const actions = convertObjectPropsToCamelCase(JSON.parse(el.dataset.actions)); + + const { learnGitlabA } = gon.experiments; + + return new Vue({ + el, + render(createElement) { + return createElement(learnGitlabA ? LearnGitlabA : LearnGitlabB, { props: { actions } }); + }, + }); +} + +initLearnGitlab(); diff --git a/app/assets/javascripts/pages/projects/merge_requests/conflicts/index.js b/app/assets/javascripts/pages/projects/merge_requests/conflicts/index.js index 28641104c5..05019915fc 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/conflicts/index.js +++ b/app/assets/javascripts/pages/projects/merge_requests/conflicts/index.js @@ -1,5 +1,5 @@ -import initSidebarBundle from '~/sidebar/sidebar_bundle'; import initMergeConflicts from '~/merge_conflicts/merge_conflicts_bundle'; +import initSidebarBundle from '~/sidebar/sidebar_bundle'; document.addEventListener('DOMContentLoaded', () => { initSidebarBundle(); diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/index.js b/app/assets/javascripts/pages/projects/merge_requests/creations/index.js index febfecebbd..34d9fa03d2 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/creations/index.js +++ b/app/assets/javascripts/pages/projects/merge_requests/creations/index.js @@ -1,3 +1,3 @@ import initMergeRequest from '~/pages/projects/merge_requests/init_merge_request'; -document.addEventListener('DOMContentLoaded', initMergeRequest); +initMergeRequest(); diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js index eb2692c7cb..1a0fa6e544 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js +++ b/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js @@ -1,7 +1,7 @@ import $ from 'jquery'; -import { localTimeAgo } from '~/lib/utils/datetime_utility'; -import axios from '~/lib/utils/axios_utils'; import initCompareAutocomplete from '~/compare_autocomplete'; +import axios from '~/lib/utils/axios_utils'; +import { localTimeAgo } from '~/lib/utils/datetime_utility'; import initTargetProjectDropdown from './target_project_dropdown'; const updateCommitList = (url, $loadingIndicator, $commitList, params) => { diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js index 01a0b4870c..9aecd15448 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js +++ b/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js @@ -1,17 +1,15 @@ -import MergeRequest from '~/merge_request'; import initPipelines from '~/commit/pipelines/pipelines_bundle'; +import MergeRequest from '~/merge_request'; import initCompare from './compare'; -document.addEventListener('DOMContentLoaded', () => { - const mrNewCompareNode = document.querySelector('.js-merge-request-new-compare'); - if (mrNewCompareNode) { - initCompare(mrNewCompareNode); - } else { - const mrNewSubmitNode = document.querySelector('.js-merge-request-new-submit'); - // eslint-disable-next-line no-new - new MergeRequest({ - action: mrNewSubmitNode.dataset.mrSubmitAction, - }); - initPipelines(); - } -}); +const mrNewCompareNode = document.querySelector('.js-merge-request-new-compare'); +if (mrNewCompareNode) { + initCompare(mrNewCompareNode); +} else { + const mrNewSubmitNode = document.querySelector('.js-merge-request-new-submit'); + // eslint-disable-next-line no-new + new MergeRequest({ + action: mrNewSubmitNode.dataset.mrSubmitAction, + }); + initPipelines(); +} diff --git a/app/assets/javascripts/pages/projects/merge_requests/edit/check_form_state.js b/app/assets/javascripts/pages/projects/merge_requests/edit/check_form_state.js new file mode 100644 index 0000000000..74178ab96e --- /dev/null +++ b/app/assets/javascripts/pages/projects/merge_requests/edit/check_form_state.js @@ -0,0 +1,24 @@ +import { serializeForm } from '~/lib/utils/forms'; + +const findForm = () => document.querySelector('.merge-request-form'); +const serializeFormData = () => JSON.stringify(serializeForm(findForm())); + +export default () => { + const oldFormData = serializeFormData(); + + const compareFormData = (e) => { + const newFormData = serializeFormData(); + + if (oldFormData !== newFormData) { + e.preventDefault(); + // eslint-disable-next-line no-param-reassign + e.returnValue = ''; // Chrome requires returnValue to be set + } + }; + + window.addEventListener('beforeunload', compareFormData); + + findForm().addEventListener('submit', () => + window.removeEventListener('beforeunload', compareFormData), + ); +}; diff --git a/app/assets/javascripts/pages/projects/merge_requests/edit/index.js b/app/assets/javascripts/pages/projects/merge_requests/edit/index.js index febfecebbd..399aebb0c8 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/edit/index.js +++ b/app/assets/javascripts/pages/projects/merge_requests/edit/index.js @@ -1,3 +1,7 @@ import initMergeRequest from '~/pages/projects/merge_requests/init_merge_request'; +import initCheckFormState from './check_form_state'; -document.addEventListener('DOMContentLoaded', initMergeRequest); +document.addEventListener('DOMContentLoaded', () => { + initMergeRequest(); + initCheckFormState(); +}); diff --git a/app/assets/javascripts/pages/projects/merge_requests/index/index.js b/app/assets/javascripts/pages/projects/merge_requests/index/index.js index 94a12cc270..76705256fe 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/index/index.js +++ b/app/assets/javascripts/pages/projects/merge_requests/index/index.js @@ -1,11 +1,12 @@ import addExtraTokensForMergeRequests from 'ee_else_ce/filtered_search/add_extra_tokens_for_merge_requests'; -import IssuableIndex from '~/issuable_index'; import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; -import UsersSelect from '~/users_select'; -import initFilteredSearch from '~/pages/search/init_filtered_search'; import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys'; +import initIssuableByEmail from '~/issuable/init_issuable_by_email'; +import IssuableIndex from '~/issuable_index'; import { FILTERED_SEARCH } from '~/pages/constants'; import { ISSUABLE_INDEX } from '~/pages/projects/constants'; +import initFilteredSearch from '~/pages/search/init_filtered_search'; +import UsersSelect from '~/users_select'; new IssuableIndex(ISSUABLE_INDEX.MERGE_REQUEST); // eslint-disable-line no-new @@ -19,3 +20,5 @@ initFilteredSearch({ new UsersSelect(); // eslint-disable-line no-new new ShortcutsNavigation(); // eslint-disable-line no-new + +initIssuableByEmail(); diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js index 76d72efb11..7d5719cf8a 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js +++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js @@ -2,8 +2,8 @@ import $ from 'jquery'; import IssuableForm from 'ee_else_ce/issuable_form'; -import Diff from '~/diff'; import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; +import Diff from '~/diff'; import GLForm from '~/gl_form'; import LabelsSelect from '~/labels_select'; import MilestoneSelect from '~/milestone_select'; diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js index 1a0c586099..d4d5e9f271 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js +++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js @@ -1,16 +1,16 @@ import Vue from 'vue'; -import ZenMode from '~/zen_mode'; -import initIssuableSidebar from '~/init_issuable_sidebar'; -import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable'; -import { handleLocationHash } from '~/lib/utils/common_utils'; -import initPipelines from '~/commit/pipelines/pipelines_bundle'; -import initSourcegraph from '~/sourcegraph'; import loadAwardsHandler from '~/awards_handler'; -import initInviteMemberTrigger from '~/invite_member/init_invite_member_trigger'; +import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable'; +import initPipelines from '~/commit/pipelines/pipelines_bundle'; +import initIssuableSidebar from '~/init_issuable_sidebar'; import initInviteMemberModal from '~/invite_member/init_invite_member_modal'; +import initInviteMemberTrigger from '~/invite_member/init_invite_member_trigger'; +import { handleLocationHash } from '~/lib/utils/common_utils'; import StatusBox from '~/merge_request/components/status_box.vue'; +import initSourcegraph from '~/sourcegraph'; +import ZenMode from '~/zen_mode'; -export default function () { +export default function initMergeRequestShow() { new ZenMode(); // eslint-disable-line no-new initIssuableSidebar(); initPipelines(); diff --git a/app/assets/javascripts/pages/projects/merge_requests/show/index.js b/app/assets/javascripts/pages/projects/merge_requests/show/index.js index 602d749ee0..546fa66eda 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/show/index.js +++ b/app/assets/javascripts/pages/projects/merge_requests/show/index.js @@ -1,14 +1,12 @@ -import initMrNotes from '~/mr_notes'; import { initReviewBar } from '~/batch_comments'; -import initSidebarBundle from '~/sidebar/sidebar_bundle'; -import initShow from '../init_merge_request_show'; -import initIssuableHeaderWarning from '~/vue_shared/components/issuable/init_issuable_header_warning'; +import initMrNotes from '~/mr_notes'; import store from '~/mr_notes/stores'; +import initSidebarBundle from '~/sidebar/sidebar_bundle'; +import initIssuableHeaderWarning from '~/vue_shared/components/issuable/init_issuable_header_warning'; +import initShow from '../init_merge_request_show'; initShow(); -if (gon.features && !gon.features.vueIssuableSidebar) { - initSidebarBundle(); -} +initSidebarBundle(); initMrNotes(); initReviewBar(); initIssuableHeaderWarning(store); diff --git a/app/assets/javascripts/pages/projects/milestones/show/index.js b/app/assets/javascripts/pages/projects/milestones/show/index.js index 84a5242159..a853413e1f 100644 --- a/app/assets/javascripts/pages/projects/milestones/show/index.js +++ b/app/assets/javascripts/pages/projects/milestones/show/index.js @@ -1,5 +1,5 @@ -import initMilestonesShow from '~/pages/milestones/shared/init_milestones_show'; import milestones from '~/pages/milestones/shared'; +import initMilestonesShow from '~/pages/milestones/shared/init_milestones_show'; document.addEventListener('DOMContentLoaded', () => { initMilestonesShow(); diff --git a/app/assets/javascripts/pages/projects/new/index.js b/app/assets/javascripts/pages/projects/new/index.js index 88f4db3ec0..437594fdf1 100644 --- a/app/assets/javascripts/pages/projects/new/index.js +++ b/app/assets/javascripts/pages/projects/new/index.js @@ -1,7 +1,7 @@ +import { deprecatedCreateFlash as createFlash } from '~/flash'; +import { __ } from '~/locale'; import initProjectVisibilitySelector from '../../../project_visibility'; import initProjectNew from '../../../projects/project_new'; -import { __ } from '~/locale'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; document.addEventListener('DOMContentLoaded', () => { initProjectVisibilitySelector(); diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue index aa7414f3ae..3b19231720 100644 --- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue @@ -1,7 +1,7 @@ + + diff --git a/app/assets/javascripts/pipeline_editor/components/editor/ci_config_merged_preview.vue b/app/assets/javascripts/pipeline_editor/components/editor/ci_config_merged_preview.vue new file mode 100644 index 0000000000..007faa4ed0 --- /dev/null +++ b/app/assets/javascripts/pipeline_editor/components/editor/ci_config_merged_preview.vue @@ -0,0 +1,100 @@ + + diff --git a/app/assets/javascripts/pipeline_editor/components/text_editor.vue b/app/assets/javascripts/pipeline_editor/components/editor/text_editor.vue similarity index 61% rename from app/assets/javascripts/pipeline_editor/components/text_editor.vue rename to app/assets/javascripts/pipeline_editor/components/editor/text_editor.vue index b8d49d77ea..872da88d3e 100644 --- a/app/assets/javascripts/pipeline_editor/components/text_editor.vue +++ b/app/assets/javascripts/pipeline_editor/components/editor/text_editor.vue @@ -1,26 +1,30 @@ diff --git a/app/assets/javascripts/registry/explorer/router.js b/app/assets/javascripts/registry/explorer/router.js index d8903cf093..a0c4417d54 100644 --- a/app/assets/javascripts/registry/explorer/router.js +++ b/app/assets/javascripts/registry/explorer/router.js @@ -1,8 +1,8 @@ import Vue from 'vue'; import VueRouter from 'vue-router'; -import List from './pages/list.vue'; -import Details from './pages/details.vue'; import { CONTAINER_REGISTRY_TITLE } from './constants/index'; +import Details from './pages/details.vue'; +import List from './pages/list.vue'; Vue.use(VueRouter); diff --git a/app/assets/javascripts/registry/settings/components/registry_settings_app.vue b/app/assets/javascripts/registry/settings/components/registry_settings_app.vue index 66eb681784..480590ec71 100644 --- a/app/assets/javascripts/registry/settings/components/registry_settings_app.vue +++ b/app/assets/javascripts/registry/settings/components/registry_settings_app.vue @@ -1,7 +1,6 @@ + + diff --git a/app/assets/javascripts/search/sort/constants.js b/app/assets/javascripts/search/sort/constants.js new file mode 100644 index 0000000000..575fba5873 --- /dev/null +++ b/app/assets/javascripts/search/sort/constants.js @@ -0,0 +1,19 @@ +import { __ } from '~/locale'; + +export const SORT_DIRECTION_UI = { + disabled: { + direction: null, + tooltip: '', + icon: 'sort-highest', + }, + desc: { + direction: 'desc', + tooltip: __('Sort direction: Descending'), + icon: 'sort-highest', + }, + asc: { + direction: 'asc', + tooltip: __('Sort direction: Ascending'), + icon: 'sort-lowest', + }, +}; diff --git a/app/assets/javascripts/search/sort/index.js b/app/assets/javascripts/search/sort/index.js new file mode 100644 index 0000000000..84bb5175b1 --- /dev/null +++ b/app/assets/javascripts/search/sort/index.js @@ -0,0 +1,27 @@ +import Vue from 'vue'; +import Translate from '~/vue_shared/translate'; +import GlobalSearchSort from './components/app.vue'; + +Vue.use(Translate); + +export const initSearchSort = (store) => { + const el = document.getElementById('js-search-sort'); + + if (!el) return false; + + let { searchSortOptions } = el.dataset; + + searchSortOptions = JSON.parse(searchSortOptions); + + return new Vue({ + el, + store, + render(createElement) { + return createElement(GlobalSearchSort, { + props: { + searchSortOptions, + }, + }); + }, + }); +}; diff --git a/app/assets/javascripts/search/store/actions.js b/app/assets/javascripts/search/store/actions.js index bdfe966d99..0af679644f 100644 --- a/app/assets/javascripts/search/store/actions.js +++ b/app/assets/javascripts/search/store/actions.js @@ -1,7 +1,7 @@ import Api from '~/api'; import createFlash from '~/flash'; -import { __ } from '~/locale'; import { visitUrl, setUrlParams } from '~/lib/utils/url_utility'; +import { __ } from '~/locale'; import * as types from './mutation_types'; export const fetchGroups = ({ commit }, search) => { diff --git a/app/assets/javascripts/search/topbar/components/app.vue b/app/assets/javascripts/search/topbar/components/app.vue new file mode 100644 index 0000000000..987735ed81 --- /dev/null +++ b/app/assets/javascripts/search/topbar/components/app.vue @@ -0,0 +1,73 @@ + + + diff --git a/app/assets/javascripts/search/topbar/components/group_filter.vue b/app/assets/javascripts/search/topbar/components/group_filter.vue index fce9ec17d2..2acab4e805 100644 --- a/app/assets/javascripts/search/topbar/components/group_filter.vue +++ b/app/assets/javascripts/search/topbar/components/group_filter.vue @@ -1,9 +1,9 @@ diff --git a/app/assets/javascripts/search_settings/index.js b/app/assets/javascripts/search_settings/index.js index 1fb1a378ff..676c43c563 100644 --- a/app/assets/javascripts/search_settings/index.js +++ b/app/assets/javascripts/search_settings/index.js @@ -1,23 +1,10 @@ -import Vue from 'vue'; -import $ from 'jquery'; -import { expandSection, closeSection } from '~/settings_panels'; -import SearchSettings from '~/search_settings/components/search_settings.vue'; +const initSearch = async () => { + const el = document.querySelector('.js-search-settings-app'); -const initSearch = ({ el }) => - new Vue({ - el, - render: (h) => - h(SearchSettings, { - ref: 'searchSettings', - props: { - searchRoot: document.querySelector('#content-body'), - sectionSelector: 'section.settings', - }, - on: { - collapse: (section) => closeSection($(section)), - expand: (section) => expandSection($(section)), - }, - }), - }); + if (el) { + const { default: mount } = await import(/* webpackChunkName: 'search_settings' */ './mount'); + mount({ el }); + } +}; export default initSearch; diff --git a/app/assets/javascripts/search_settings/mount.js b/app/assets/javascripts/search_settings/mount.js new file mode 100644 index 0000000000..b1086f9ca1 --- /dev/null +++ b/app/assets/javascripts/search_settings/mount.js @@ -0,0 +1,23 @@ +import Vue from 'vue'; +import SearchSettings from '~/search_settings/components/search_settings.vue'; +import { expandSection, closeSection, isExpanded } from '~/settings_panels'; + +const mountSearch = ({ el }) => + new Vue({ + el, + render: (h) => + h(SearchSettings, { + ref: 'searchSettings', + props: { + searchRoot: document.querySelector('#content-body'), + sectionSelector: '.js-search-settings-section, section.settings', + isExpandedFn: isExpanded, + }, + on: { + collapse: closeSection, + expand: expandSection, + }, + }), + }); + +export default mountSearch; diff --git a/app/assets/javascripts/security_configuration/components/app.vue b/app/assets/javascripts/security_configuration/components/app.vue new file mode 100644 index 0000000000..513a7353d2 --- /dev/null +++ b/app/assets/javascripts/security_configuration/components/app.vue @@ -0,0 +1,23 @@ + + + diff --git a/app/assets/javascripts/security_configuration/components/configuration_table.vue b/app/assets/javascripts/security_configuration/components/configuration_table.vue new file mode 100644 index 0000000000..9475cc1781 --- /dev/null +++ b/app/assets/javascripts/security_configuration/components/configuration_table.vue @@ -0,0 +1,97 @@ + + + diff --git a/app/assets/javascripts/security_configuration/components/features_constants.js b/app/assets/javascripts/security_configuration/components/features_constants.js new file mode 100644 index 0000000000..d846a2761d --- /dev/null +++ b/app/assets/javascripts/security_configuration/components/features_constants.js @@ -0,0 +1,112 @@ +import { helpPagePath } from '~/helpers/help_page_helper'; +import { s__ } from '~/locale'; + +import { + REPORT_TYPE_SAST, + REPORT_TYPE_DAST, + REPORT_TYPE_SECRET_DETECTION, + REPORT_TYPE_DEPENDENCY_SCANNING, + REPORT_TYPE_CONTAINER_SCANNING, + REPORT_TYPE_COVERAGE_FUZZING, + REPORT_TYPE_LICENSE_COMPLIANCE, +} from '~/vue_shared/security_reports/constants'; + +/** + * Translations & helpPagePaths for Static Security Configuration Page + */ +export const SAST_NAME = s__('Static Application Security Testing (SAST)'); +export const SAST_DESCRIPTION = s__('Analyze your source code for known vulnerabilities.'); +export const SAST_HELP_PATH = helpPagePath('user/application_security/sast/index'); + +export const DAST_NAME = s__('Dynamic Application Security Testing (DAST)'); +export const DAST_DESCRIPTION = s__('Analyze a review version of your web application.'); +export const DAST_HELP_PATH = helpPagePath('user/application_security/dast/index'); + +export const SECRET_DETECTION_NAME = s__('Secret Detection'); +export const SECRET_DETECTION_DESCRIPTION = s__( + 'Analyze your source code and git history for secrets.', +); +export const SECRET_DETECTION_HELP_PATH = helpPagePath( + 'user/application_security/secret_detection/index', +); + +export const DEPENDENCY_SCANNING_NAME = s__('Dependency Scanning'); +export const DEPENDENCY_SCANNING_DESCRIPTION = s__( + 'Analyze your dependencies for known vulnerabilities.', +); +export const DEPENDENCY_SCANNING_HELP_PATH = helpPagePath( + 'user/application_security/dependency_scanning/index', +); + +export const CONTAINER_SCANNING_NAME = s__('Container Scanning'); +export const CONTAINER_SCANNING_DESCRIPTION = s__( + 'Check your Docker images for known vulnerabilities.', +); +export const CONTAINER_SCANNING_HELP_PATH = helpPagePath( + 'user/application_security/container_scanning/index', +); + +export const COVERAGE_FUZZING_NAME = s__('Coverage Fuzzing'); +export const COVERAGE_FUZZING_DESCRIPTION = s__( + 'Find bugs in your code with coverage-guided fuzzing.', +); +export const COVERAGE_FUZZING_HELP_PATH = helpPagePath( + 'user/application_security/coverage_fuzzing/index', +); + +export const LICENSE_COMPLIANCE_NAME = s__('License Compliance'); +export const LICENSE_COMPLIANCE_DESCRIPTION = s__( + 'Search your project dependencies for their licenses and apply policies.', +); +export const LICENSE_COMPLIANCE_HELP_PATH = helpPagePath( + 'user/compliance/license_compliance/index', +); + +export const UPGRADE_CTA = s__( + 'SecurityConfiguration|Available with %{linkStart}upgrade or free trial%{linkEnd}', +); + +export const features = [ + { + name: SAST_NAME, + description: SAST_DESCRIPTION, + helpPath: SAST_HELP_PATH, + type: REPORT_TYPE_SAST, + }, + { + name: DAST_NAME, + description: DAST_DESCRIPTION, + helpPath: DAST_HELP_PATH, + type: REPORT_TYPE_DAST, + }, + { + name: SECRET_DETECTION_NAME, + description: SECRET_DETECTION_DESCRIPTION, + helpPath: SECRET_DETECTION_HELP_PATH, + type: REPORT_TYPE_SECRET_DETECTION, + }, + { + name: DEPENDENCY_SCANNING_NAME, + description: DEPENDENCY_SCANNING_DESCRIPTION, + helpPath: DEPENDENCY_SCANNING_HELP_PATH, + type: REPORT_TYPE_DEPENDENCY_SCANNING, + }, + { + name: CONTAINER_SCANNING_NAME, + description: CONTAINER_SCANNING_DESCRIPTION, + helpPath: CONTAINER_SCANNING_HELP_PATH, + type: REPORT_TYPE_CONTAINER_SCANNING, + }, + { + name: COVERAGE_FUZZING_NAME, + description: COVERAGE_FUZZING_DESCRIPTION, + helpPath: COVERAGE_FUZZING_HELP_PATH, + type: REPORT_TYPE_COVERAGE_FUZZING, + }, + { + name: LICENSE_COMPLIANCE_NAME, + description: LICENSE_COMPLIANCE_DESCRIPTION, + helpPath: LICENSE_COMPLIANCE_HELP_PATH, + type: REPORT_TYPE_LICENSE_COMPLIANCE, + }, +]; diff --git a/app/assets/javascripts/security_configuration/components/manage_sast.vue b/app/assets/javascripts/security_configuration/components/manage_sast.vue new file mode 100644 index 0000000000..5169096d56 --- /dev/null +++ b/app/assets/javascripts/security_configuration/components/manage_sast.vue @@ -0,0 +1,57 @@ + + + diff --git a/app/assets/javascripts/security_configuration/components/upgrade.vue b/app/assets/javascripts/security_configuration/components/upgrade.vue new file mode 100644 index 0000000000..166ee4ff19 --- /dev/null +++ b/app/assets/javascripts/security_configuration/components/upgrade.vue @@ -0,0 +1,26 @@ + + + diff --git a/app/assets/javascripts/security_configuration/graphql/configure_sast.mutation.graphql b/app/assets/javascripts/security_configuration/graphql/configure_sast.mutation.graphql new file mode 100644 index 0000000000..9e826cf9e4 --- /dev/null +++ b/app/assets/javascripts/security_configuration/graphql/configure_sast.mutation.graphql @@ -0,0 +1,6 @@ +mutation configureSast($input: ConfigureSastInput!) { + configureSast(input: $input) { + successPath + errors + } +} diff --git a/app/assets/javascripts/security_configuration/index.js b/app/assets/javascripts/security_configuration/index.js new file mode 100644 index 0000000000..c98fa46b32 --- /dev/null +++ b/app/assets/javascripts/security_configuration/index.js @@ -0,0 +1,29 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; +import SecurityConfigurationApp from './components/app.vue'; + +export const initStaticSecurityConfiguration = (el) => { + if (!el) { + return null; + } + + Vue.use(VueApollo); + + const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), + }); + + const { projectPath } = el.dataset; + + return new Vue({ + el, + apolloProvider, + provide: { + projectPath, + }, + render(createElement) { + return createElement(SecurityConfigurationApp); + }, + }); +}; diff --git a/app/assets/javascripts/self_monitor/components/self_monitor_form.vue b/app/assets/javascripts/self_monitor/components/self_monitor_form.vue index 6776a9ebb2..9b7264e92c 100644 --- a/app/assets/javascripts/self_monitor/components/self_monitor_form.vue +++ b/app/assets/javascripts/self_monitor/components/self_monitor_form.vue @@ -1,10 +1,11 @@ - - diff --git a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue index c8efbd73b4..bed264341a 100644 --- a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue +++ b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue @@ -1,15 +1,16 @@ - diff --git a/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue b/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue index b713b0f960..20667e695c 100644 --- a/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue +++ b/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue @@ -1,11 +1,30 @@ @@ -123,7 +135,7 @@ export default { +import { + GlDropdownItem, + GlDropdownDivider, + GlAvatarLabeled, + GlAvatarLink, + GlSearchBoxByType, + GlLoadingIcon, +} from '@gitlab/ui'; +import { cloneDeep } from 'lodash'; +import Vue from 'vue'; +import createFlash from '~/flash'; +import searchUsers from '~/graphql_shared/queries/users_search.query.graphql'; +import { IssuableType } from '~/issue_show/constants'; +import { __, n__ } from '~/locale'; +import IssuableAssignees from '~/sidebar/components/assignees/issuable_assignees.vue'; +import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue'; +import { assigneesQueries } from '~/sidebar/constants'; +import MultiSelectDropdown from '~/vue_shared/components/sidebar/multiselect_dropdown.vue'; + +export const assigneesWidget = Vue.observable({ + updateAssignees: null, +}); + +export default { + i18n: { + unassigned: __('Unassigned'), + assignee: __('Assignee'), + assignees: __('Assignees'), + assignTo: __('Assign to'), + }, + assigneesQueries, + components: { + SidebarEditableItem, + IssuableAssignees, + MultiSelectDropdown, + GlDropdownItem, + GlDropdownDivider, + GlAvatarLabeled, + GlAvatarLink, + GlSearchBoxByType, + GlLoadingIcon, + }, + props: { + iid: { + type: String, + required: true, + }, + fullPath: { + type: String, + required: true, + }, + initialAssignees: { + type: Array, + required: false, + default: null, + }, + issuableType: { + type: String, + required: false, + default: IssuableType.Issue, + validator(value) { + return [IssuableType.Issue, IssuableType.MergeRequest].includes(value); + }, + }, + multipleAssignees: { + type: Boolean, + required: false, + default: true, + }, + }, + data() { + return { + search: '', + issuable: {}, + searchUsers: [], + selected: [], + isSettingAssignees: false, + isSearching: false, + }; + }, + apollo: { + issuable: { + query() { + return this.$options.assigneesQueries[this.issuableType].query; + }, + variables() { + return this.queryVariables; + }, + update(data) { + return data.issuable || data.project?.issuable; + }, + result({ data }) { + const issuable = data.issuable || data.project?.issuable; + if (issuable) { + this.selected = this.moveCurrentUserToStart(cloneDeep(issuable.assignees.nodes)); + } + }, + error() { + createFlash({ message: __('An error occurred while fetching participants.') }); + }, + }, + searchUsers: { + query: searchUsers, + variables() { + return { + search: this.search, + }; + }, + update(data) { + return data.users?.nodes || []; + }, + debounce: 250, + skip() { + return this.isSearchEmpty; + }, + error() { + createFlash({ message: __('An error occurred while searching users.') }); + this.isSearching = false; + }, + result() { + this.isSearching = false; + }, + }, + }, + computed: { + queryVariables() { + return { + iid: this.iid, + fullPath: this.fullPath, + }; + }, + assignees() { + const currentAssignees = this.$apollo.queries.issuable.loading + ? this.initialAssignees + : this.issuable?.assignees?.nodes; + return currentAssignees || []; + }, + participants() { + const users = + this.isSearchEmpty || this.isSearching + ? this.issuable?.participants?.nodes + : this.searchUsers; + return this.moveCurrentUserToStart(users); + }, + assigneeText() { + const items = this.$apollo.queries.issuable.loading ? this.initialAssignees : this.selected; + return n__('Assignee', '%d Assignees', items.length); + }, + selectedFiltered() { + if (this.isSearchEmpty || this.isSearching) { + return this.selected; + } + + const foundUsernames = this.searchUsers.map(({ username }) => username); + return this.selected.filter(({ username }) => foundUsernames.includes(username)); + }, + unselectedFiltered() { + return ( + this.participants?.filter(({ username }) => !this.selectedUserNames.includes(username)) || + [] + ); + }, + selectedIsEmpty() { + return this.selectedFiltered.length === 0; + }, + selectedUserNames() { + return this.selected.map(({ username }) => username); + }, + isSearchEmpty() { + return this.search === ''; + }, + currentUser() { + return { + username: gon?.current_username, + name: gon?.current_user_fullname, + avatarUrl: gon?.current_user_avatar_url, + }; + }, + isAssigneesLoading() { + return !this.initialAssignees && this.$apollo.queries.issuable.loading; + }, + isCurrentUserInParticipants() { + const isCurrentUser = (user) => user.username === this.currentUser.username; + return this.selected.some(isCurrentUser) || this.participants.some(isCurrentUser); + }, + noUsersFound() { + return !this.isSearchEmpty && this.unselectedFiltered.length === 0; + }, + showCurrentUser() { + return !this.isCurrentUserInParticipants && (this.isSearchEmpty || this.isSearching); + }, + }, + watch: { + // We need to add this watcher to track the moment when user is alredy typing + // but query is still not started due to debounce + search(newVal) { + if (newVal) { + this.isSearching = true; + } + }, + }, + created() { + assigneesWidget.updateAssignees = this.updateAssignees; + }, + destroyed() { + assigneesWidget.updateAssignees = null; + }, + methods: { + updateAssignees(assigneeUsernames) { + this.isSettingAssignees = true; + return this.$apollo + .mutate({ + mutation: this.$options.assigneesQueries[this.issuableType].mutation, + variables: { + ...this.queryVariables, + assigneeUsernames, + }, + }) + .then(({ data }) => { + this.$emit('assignees-updated', data); + return data; + }) + .catch(() => { + createFlash({ message: __('An error occurred while updating assignees.') }); + }) + .finally(() => { + this.isSettingAssignees = false; + }); + }, + selectAssignee(name) { + if (name === undefined) { + this.clearSelected(); + return; + } + + if (!this.multipleAssignees) { + this.selected = [name]; + this.collapseWidget(); + } else { + this.selected = this.selected.concat(name); + } + }, + unselect(name) { + this.selected = this.selected.filter((user) => user.username !== name); + + if (!this.multipleAssignees) { + this.collapseWidget(); + } + }, + assignSelf() { + this.updateAssignees(this.currentUser.username); + }, + clearSelected() { + this.selected = []; + }, + saveAssignees() { + this.updateAssignees(this.selectedUserNames); + }, + isChecked(id) { + return this.selectedUserNames.includes(id); + }, + async focusSearch() { + await this.$nextTick(); + this.$refs.search.focusInput(); + }, + moveCurrentUserToStart(users) { + if (!users) { + return []; + } + const usersCopy = [...users]; + const currentUser = usersCopy.find((user) => user.username === this.currentUser.username); + + if (currentUser) { + const index = usersCopy.indexOf(currentUser); + usersCopy.splice(0, 0, usersCopy.splice(index, 1)[0]); + } + + return usersCopy; + }, + collapseWidget() { + this.$refs.toggle.collapse(); + }, + }, +}; + + + diff --git a/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue index 31d5d7c007..3677564880 100644 --- a/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue +++ b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue @@ -1,12 +1,14 @@ @@ -68,7 +73,7 @@ export default { :issuable-type="issuableType" >
    -
    {{ firstUser.name }}
    +
    {{ username }}
    diff --git a/app/assets/javascripts/sidebar/components/assignees/user_name_with_status.vue b/app/assets/javascripts/sidebar/components/assignees/user_name_with_status.vue new file mode 100644 index 0000000000..41b3b6c9a4 --- /dev/null +++ b/app/assets/javascripts/sidebar/components/assignees/user_name_with_status.vue @@ -0,0 +1,40 @@ + + diff --git a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue index ce120ff82f..57b3705e80 100644 --- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue +++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue @@ -1,6 +1,6 @@ @@ -56,7 +59,7 @@ export default {
    @@ -66,6 +69,7 @@ export default { :users="sortedReviewers" :root-path="rootPath" :issuable-type="issuableType" + @request-review="requestReview" />
    diff --git a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue index 1a2473e5f6..b5cf5df495 100644 --- a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue +++ b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue @@ -1,14 +1,14 @@ @@ -101,6 +104,7 @@ export default { :editable="store.editable" :issuable-type="issuableType" class="value" + @request-review="requestReview" /> diff --git a/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue b/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue index e82a271d00..cbd68f2513 100644 --- a/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue +++ b/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue @@ -1,15 +1,19 @@ diff --git a/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue b/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue index 0cf11e8334..6a6300dcde 100644 --- a/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue +++ b/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue @@ -7,10 +7,10 @@ import { GlSprintf, GlLink, } from '@gitlab/ui'; +import createFlash from '~/flash'; import { INCIDENT_SEVERITY, ISSUABLE_TYPES, I18N } from './constants'; import updateIssuableSeverity from './graphql/mutations/update_issuable_severity.mutation.graphql'; import SeverityToken from './severity.vue'; -import createFlash from '~/flash'; export default { i18n: I18N, diff --git a/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue b/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue new file mode 100644 index 0000000000..9da839cd13 --- /dev/null +++ b/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue @@ -0,0 +1,95 @@ + + + diff --git a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue index ee1c98e9d6..3ad097138a 100644 --- a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue +++ b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue @@ -1,7 +1,7 @@