From 46d6051816075f70d8c42efd92c6c6e91071fe07 Mon Sep 17 00:00:00 2001
From: Janis Altherr <jaltherr@gitlab.com>
Date: Tue, 12 Jul 2022 19:22:12 +0000
Subject: [PATCH] Add mark_onboarding_complete mutation and FE files for pages
 pipeline wizard

This commit introduces a GraphQL Mutation allowing
a once-off toggle of the "onboarding_complete"
property in Pages metadata.

This commit introduces the frontend for an onboarding view for Pages
that will enable the user to configure their .gitlab-ci.yml file
directly in the UI.
This commit needs a follow up MR to be displayed by the UI.
---
 .../components/pages_pipeline_wizard.vue      |  84 +++++++++++++++
 .../queries/mark_onboarding_complete.graphql  |   6 ++
 .../pipeline_wizard/components/wrapper.vue    |   1 +
 .../pipeline_wizard/pipeline_wizard.vue       |   1 +
 .../pipeline_wizard/templates/.gitkeep        |   0
 .../pipeline_wizard/templates/pages.yml       |  53 +++++++++
 app/graphql/mutations/pages/base.rb           |  13 +++
 .../pages/mark_onboarding_complete.rb         |  27 +++++
 app/graphql/types/mutation_type.rb            |   1 +
 doc/api/graphql/reference/index.md            |  19 ++++
 jest.config.base.js                           |   5 +-
 locale/gitlab.pot                             |   3 +
 .../new/pages/pages_pipeline_wizard_spec.js   | 102 ++++++++++++++++++
 .../pipeline_wizard/pipeline_wizard_spec.js   |   8 ++
 .../pipeline_wizard/templates/pages_spec.js   |  89 +++++++++++++++
 .../pages/mark_onboarding_complete_spec.rb    |  57 ++++++++++
 16 files changed, 467 insertions(+), 2 deletions(-)
 create mode 100644 app/assets/javascripts/gitlab_pages/components/pages_pipeline_wizard.vue
 create mode 100644 app/assets/javascripts/gitlab_pages/queries/mark_onboarding_complete.graphql
 delete mode 100644 app/assets/javascripts/pipeline_wizard/templates/.gitkeep
 create mode 100644 app/assets/javascripts/pipeline_wizard/templates/pages.yml
 create mode 100644 app/graphql/mutations/pages/base.rb
 create mode 100644 app/graphql/mutations/pages/mark_onboarding_complete.rb
 create mode 100644 spec/frontend/gitlab_pages/new/pages/pages_pipeline_wizard_spec.js
 create mode 100644 spec/frontend/pipeline_wizard/templates/pages_spec.js
 create mode 100644 spec/graphql/mutations/pages/mark_onboarding_complete_spec.rb

diff --git a/app/assets/javascripts/gitlab_pages/components/pages_pipeline_wizard.vue b/app/assets/javascripts/gitlab_pages/components/pages_pipeline_wizard.vue
new file mode 100644
index 000000000000..f17a05999b01
--- /dev/null
+++ b/app/assets/javascripts/gitlab_pages/components/pages_pipeline_wizard.vue
@@ -0,0 +1,84 @@
+<script>
+import { GlLoadingIcon } from '@gitlab/ui';
+import { captureException } from '@sentry/browser';
+import PipelineWizard from '~/pipeline_wizard/pipeline_wizard.vue';
+import PagesWizardTemplate from '~/pipeline_wizard/templates/pages.yml';
+import { logError } from '~/lib/logger';
+import { s__ } from '~/locale';
+import { redirectTo } from '~/lib/utils/url_utility';
+import pagesMarkOnboardingComplete from '../queries/mark_onboarding_complete.graphql';
+
+export const i18n = {
+  loadingMessage: s__('GitLabPages|Updating your Pages configuration...'),
+};
+
+export default {
+  name: 'PagesPipelineWizard',
+  i18n,
+  PagesWizardTemplate,
+  components: {
+    PipelineWizard,
+    GlLoadingIcon,
+  },
+  props: {
+    projectPath: {
+      type: String,
+      required: true,
+    },
+    defaultBranch: {
+      type: String,
+      required: true,
+    },
+    redirectToWhenDone: {
+      type: String,
+      required: true,
+    },
+  },
+  data() {
+    return {
+      loading: false,
+    };
+  },
+  methods: {
+    async updateOnboardingState() {
+      try {
+        await this.$apollo.mutate({
+          mutation: pagesMarkOnboardingComplete,
+          variables: {
+            input: { projectPath: this.projectPath },
+          },
+        });
+      } catch (e) {
+        // eslint-disable-next-line @gitlab/require-i18n-strings
+        logError('Updating the pages onboarding state failed', e);
+        captureException(e);
+      }
+    },
+    async onDone() {
+      this.loading = true;
+      await this.updateOnboardingState();
+      redirectTo(this.redirectToWhenDone);
+    },
+  },
+};
+</script>
+
+<template>
+  <div>
+    <div
+      v-if="loading"
+      class="gl-p-3 gl-rounded-base gl-text-center"
+      data-testid="onboarding-mutation-loading"
+    >
+      <gl-loading-icon />
+      {{ $options.i18n.loadingMessage }}
+    </div>
+    <pipeline-wizard
+      v-else
+      :template="$options.PagesWizardTemplate"
+      :project-path="projectPath"
+      :default-branch="defaultBranch"
+      @done="onDone"
+    />
+  </div>
+</template>
diff --git a/app/assets/javascripts/gitlab_pages/queries/mark_onboarding_complete.graphql b/app/assets/javascripts/gitlab_pages/queries/mark_onboarding_complete.graphql
new file mode 100644
index 000000000000..abedd54b0793
--- /dev/null
+++ b/app/assets/javascripts/gitlab_pages/queries/mark_onboarding_complete.graphql
@@ -0,0 +1,6 @@
+mutation pagesMarkOnboardingComplete($input: PagesMarkOnboardingCompleteInput!) {
+  pagesMarkOnboardingComplete(input: $input) {
+    onboardingComplete
+    errors
+  }
+}
diff --git a/app/assets/javascripts/pipeline_wizard/components/wrapper.vue b/app/assets/javascripts/pipeline_wizard/components/wrapper.vue
index f50cd1755109..0fe87bcee7bd 100644
--- a/app/assets/javascripts/pipeline_wizard/components/wrapper.vue
+++ b/app/assets/javascripts/pipeline_wizard/components/wrapper.vue
@@ -128,6 +128,7 @@ export default {
           :filename="filename"
           :project-path="projectPath"
           @back="currentStepIndex--"
+          @done="$emit('done')"
         />
         <wizard-step
           v-for="(step, i) in stepList"
diff --git a/app/assets/javascripts/pipeline_wizard/pipeline_wizard.vue b/app/assets/javascripts/pipeline_wizard/pipeline_wizard.vue
index 7200b4e3782b..939702fd1b53 100644
--- a/app/assets/javascripts/pipeline_wizard/pipeline_wizard.vue
+++ b/app/assets/javascripts/pipeline_wizard/pipeline_wizard.vue
@@ -60,6 +60,7 @@ export default {
       :filename="filename"
       :project-path="projectPath"
       :steps="steps"
+      @done="$emit('done')"
     />
   </div>
 </template>
diff --git a/app/assets/javascripts/pipeline_wizard/templates/.gitkeep b/app/assets/javascripts/pipeline_wizard/templates/.gitkeep
deleted file mode 100644
index e69de29bb2d1..000000000000
diff --git a/app/assets/javascripts/pipeline_wizard/templates/pages.yml b/app/assets/javascripts/pipeline_wizard/templates/pages.yml
new file mode 100644
index 000000000000..cd2242b1ba7b
--- /dev/null
+++ b/app/assets/javascripts/pipeline_wizard/templates/pages.yml
@@ -0,0 +1,53 @@
+title: Get started with Pages
+description: "GitLab Pages lets you deploy static websites in minutes. All you
+ need is a .gitlab-ci.yml file. Follow the below steps to
+    create one for your app now."
+steps:
+  - inputs:
+      - label: Select your build image
+        description: A Docker image that we can use to build your image
+        placeholder: node:lts
+        widget: text
+        target: $BUILD_IMAGE
+        required: true
+        pattern: "(?:[a-z]+/)?([a-z]+)(?::[0-9]+)?"
+        invalid-feedback: Please enter a valid docker image
+      - widget: checklist
+        title: "Before we begin, please check:"
+        items:
+          - text: The app's built output files are in a folder named "public"
+            help: GitLab Pages will only publish files in that folder.
+                  You may need to adjust your build engine's config.
+    template:
+      # The Docker image that will be used to build your app
+      image: $BUILD_IMAGE
+  - inputs:
+      - label: Installation Steps
+        description: "Enter the steps that need to run to set up a local build
+          environment, for example installing dependencies."
+        placeholder: npm ci
+        widget: list
+        target: $INSTALLATION_STEPS
+    template:
+      # Functions that should be executed before the build script is run
+      before_script: $INSTALLATION_STEPS
+  - inputs:
+      - label: Build Steps
+        description: "Enter the steps necessary to build a production version of
+          your application."
+        widget: list
+        target: $BUILD_STEPS
+    template:
+
+      pages:
+        script: $BUILD_STEPS
+
+        artifacts:
+          paths:
+            # The folder that contains the files to be exposed at the Page URL
+            - public
+
+        rules:
+          # This ensures that only pushes to the default branch will trigger
+          # a pages deploy
+          - if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH
diff --git a/app/graphql/mutations/pages/base.rb b/app/graphql/mutations/pages/base.rb
new file mode 100644
index 000000000000..5eb8ecdf0bac
--- /dev/null
+++ b/app/graphql/mutations/pages/base.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Mutations
+  module Pages
+    class Base < BaseMutation
+      include FindsProject
+
+      argument :project_path, GraphQL::Types::ID,
+        required: true,
+        description: 'Full path of the project.'
+    end
+  end
+end
diff --git a/app/graphql/mutations/pages/mark_onboarding_complete.rb b/app/graphql/mutations/pages/mark_onboarding_complete.rb
new file mode 100644
index 000000000000..2f5ce5db54a1
--- /dev/null
+++ b/app/graphql/mutations/pages/mark_onboarding_complete.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Mutations
+  module Pages
+    class MarkOnboardingComplete < Base
+      graphql_name 'PagesMarkOnboardingComplete'
+
+      field :onboarding_complete,
+        Boolean,
+        null: false,
+        description: "Indicates the new onboarding_complete state of the project's Pages metadata."
+
+      authorize :admin_project
+
+      def resolve(project_path:)
+        project = authorized_find!(project_path)
+
+        project.mark_pages_onboarding_complete
+
+        {
+          onboarding_complete: project.pages_metadatum.onboarding_complete,
+          errors: errors_on_object(project)
+        }
+      end
+    end
+  end
+end
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index 8642957af02f..46ab3f3f432c 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -148,6 +148,7 @@ class MutationType < BaseObject
     mount_mutation Mutations::WorkItems::UpdateTask, deprecated: { milestone: '15.1', reason: :alpha }
     mount_mutation Mutations::SavedReplies::Create
     mount_mutation Mutations::SavedReplies::Update
+    mount_mutation Mutations::Pages::MarkOnboardingComplete
     mount_mutation Mutations::SavedReplies::Destroy
   end
 end
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 4612ff76960f..e81157bdbb46 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -4021,6 +4021,25 @@ Input type: `OncallScheduleUpdateInput`
 | <a id="mutationoncallscheduleupdateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
 | <a id="mutationoncallscheduleupdateoncallschedule"></a>`oncallSchedule` | [`IncidentManagementOncallSchedule`](#incidentmanagementoncallschedule) | On-call schedule. |
 
+### `Mutation.pagesMarkOnboardingComplete`
+
+Input type: `PagesMarkOnboardingCompleteInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationpagesmarkonboardingcompleteclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationpagesmarkonboardingcompleteprojectpath"></a>`projectPath` | [`ID!`](#id) | Full path of the project. |
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationpagesmarkonboardingcompleteclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationpagesmarkonboardingcompleteerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+| <a id="mutationpagesmarkonboardingcompleteonboardingcomplete"></a>`onboardingComplete` | [`Boolean!`](#boolean) | Indicates the new onboarding_complete state of the project's Pages metadata. |
+
 ### `Mutation.pipelineCancel`
 
 Input type: `PipelineCancelInput`
diff --git a/jest.config.base.js b/jest.config.base.js
index 03f9be68f60f..d4b1ace3b2c9 100644
--- a/jest.config.base.js
+++ b/jest.config.base.js
@@ -173,8 +173,9 @@ module.exports = (path, options = {}) => {
       '^.+_worker\\.js$': './spec/frontend/__helpers__/web_worker_transformer.js',
       '^.+\\.js$': 'babel-jest',
       '^.+\\.vue$': 'vue-jest',
-      '^.+\\.yml$': './spec/frontend/__helpers__/yaml_transformer.js',
-      '^.+\\.(md|zip|png)$': 'jest-raw-loader',
+      'spec/frontend/editor/schema/ci/yaml_tests/.+\\.(yml|yaml)$':
+        './spec/frontend/__helpers__/yaml_transformer.js',
+      '^.+\\.(md|zip|png|yml|yaml)$': 'jest-raw-loader',
     },
     transformIgnorePatterns: [`node_modules/(?!(${transformIgnoreNodeModules.join('|')}))`],
     timers: 'fake',
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 35dd74bbd52a..69c5ee7999e5 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -17587,6 +17587,9 @@ msgstr ""
 msgid "GitLabPages|Unverified"
 msgstr ""
 
+msgid "GitLabPages|Updating your Pages configuration..."
+msgstr ""
+
 msgid "GitLabPages|Verified"
 msgstr ""
 
diff --git a/spec/frontend/gitlab_pages/new/pages/pages_pipeline_wizard_spec.js b/spec/frontend/gitlab_pages/new/pages/pages_pipeline_wizard_spec.js
new file mode 100644
index 000000000000..685b5144a95f
--- /dev/null
+++ b/spec/frontend/gitlab_pages/new/pages/pages_pipeline_wizard_spec.js
@@ -0,0 +1,102 @@
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import waitForPromises from 'helpers/wait_for_promises';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import PagesPipelineWizard, { i18n } from '~/gitlab_pages/components/pages_pipeline_wizard.vue';
+import PipelineWizard from '~/pipeline_wizard/pipeline_wizard.vue';
+import pagesTemplate from '~/pipeline_wizard/templates/pages.yml';
+import pagesMarkOnboardingComplete from '~/gitlab_pages/queries/mark_onboarding_complete.graphql';
+import { redirectTo } from '~/lib/utils/url_utility';
+
+Vue.use(VueApollo);
+
+jest.mock('~/lib/utils/url_utility');
+
+describe('PagesPipelineWizard', () => {
+  const markOnboardingCompleteMutationHandler = jest.fn();
+  let wrapper;
+  const props = {
+    projectPath: '/user/repo',
+    defaultBranch: 'main',
+    redirectToWhenDone: './',
+  };
+
+  const findPipelineWizardWrapper = () => wrapper.findComponent(PipelineWizard);
+  const createMockApolloProvider = () => {
+    return createMockApollo([
+      [
+        pagesMarkOnboardingComplete,
+        markOnboardingCompleteMutationHandler.mockResolvedValue({
+          data: {
+            pagesMarkOnboardingComplete: {
+              onboardingComplete: true,
+              errors: [],
+            },
+          },
+        }),
+      ],
+    ]);
+  };
+
+  const createComponent = () => {
+    wrapper = shallowMountExtended(PagesPipelineWizard, {
+      apolloProvider: createMockApolloProvider(),
+      propsData: props,
+    });
+  };
+
+  beforeEach(() => {
+    createComponent();
+  });
+
+  afterEach(() => {
+    wrapper.destroy();
+  });
+
+  it('shows the pipeline wizard', () => {
+    expect(findPipelineWizardWrapper().exists()).toBe(true);
+  });
+
+  it('passes the appropriate props', () => {
+    const pipelineWizardWrapperProps = findPipelineWizardWrapper().props();
+
+    expect(pipelineWizardWrapperProps.template).toBe(pagesTemplate);
+    expect(pipelineWizardWrapperProps.projectPath).toBe(props.projectPath);
+    expect(pipelineWizardWrapperProps.defaultBranch).toBe(props.defaultBranch);
+  });
+
+  describe('after the steps are complete', () => {
+    const mockDone = () => findPipelineWizardWrapper().vm.$emit('done');
+
+    it('shows a loading screen during the update', async () => {
+      mockDone();
+
+      await nextTick();
+
+      const loadingScreenWrapper = wrapper.findByTestId('onboarding-mutation-loading');
+      expect(loadingScreenWrapper.exists()).toBe(true);
+      expect(loadingScreenWrapper.text()).toBe(i18n.loadingMessage);
+    });
+
+    it('calls pagesMarkOnboardingComplete mutation when done', async () => {
+      mockDone();
+
+      await waitForPromises();
+
+      expect(markOnboardingCompleteMutationHandler).toHaveBeenCalledWith({
+        input: {
+          projectPath: props.projectPath,
+        },
+      });
+    });
+
+    it('navigates to the path defined in redirectToWhenDone when done', async () => {
+      mockDone();
+
+      await waitForPromises();
+
+      expect(redirectTo).toHaveBeenCalledWith(props.redirectToWhenDone);
+    });
+  });
+});
diff --git a/spec/frontend/pipeline_wizard/pipeline_wizard_spec.js b/spec/frontend/pipeline_wizard/pipeline_wizard_spec.js
index dd0304518a30..3f689ffdbc88 100644
--- a/spec/frontend/pipeline_wizard/pipeline_wizard_spec.js
+++ b/spec/frontend/pipeline_wizard/pipeline_wizard_spec.js
@@ -99,4 +99,12 @@ describe('PipelineWizard', () => {
       parseDocument(template).get('description').toString(),
     );
   });
+
+  it('bubbles the done event upwards', () => {
+    createComponent();
+
+    wrapper.findComponent(PipelineWizardWrapper).vm.$emit('done');
+
+    expect(wrapper.emitted().done.length).toBe(1);
+  });
 });
diff --git a/spec/frontend/pipeline_wizard/templates/pages_spec.js b/spec/frontend/pipeline_wizard/templates/pages_spec.js
new file mode 100644
index 000000000000..f89e8f054757
--- /dev/null
+++ b/spec/frontend/pipeline_wizard/templates/pages_spec.js
@@ -0,0 +1,89 @@
+import { Document, parseDocument } from 'yaml';
+import PagesWizardTemplate from '~/pipeline_wizard/templates/pages.yml';
+import { merge } from '~/lib/utils/yaml';
+
+const VAR_BUILD_IMAGE = '$BUILD_IMAGE';
+const VAR_INSTALLATION_STEPS = '$INSTALLATION_STEPS';
+const VAR_BUILD_STEPS = '$BUILD_STEPS';
+
+const getYaml = () => parseDocument(PagesWizardTemplate);
+const getFinalTemplate = () => {
+  const merged = new Document();
+  const yaml = getYaml();
+  yaml.toJS().steps.forEach((_, i) => {
+    merge(merged, yaml.getIn(['steps', i, 'template']));
+  });
+  return merged;
+};
+
+describe('Pages Template', () => {
+  it('is valid yaml', () => {
+    // Testing equality to an empty array (as opposed to just comparing
+    // errors.length) will cause jest to print the underlying error
+    expect(getYaml().errors).toEqual([]);
+  });
+
+  it('includes all `target`s in the respective `template`', () => {
+    const yaml = getYaml();
+    const actual = yaml.toJS().steps.map((x, i) => ({
+      inputs: x.inputs,
+      template: yaml.getIn(['steps', i, 'template']).toString(),
+    }));
+
+    expect(actual).toEqual([
+      {
+        inputs: [
+          expect.objectContaining({
+            label: 'Select your build image',
+            target: VAR_BUILD_IMAGE,
+          }),
+          expect.objectContaining({
+            widget: 'checklist',
+            title: 'Before we begin, please check:',
+          }),
+        ],
+        template: expect.stringContaining(VAR_BUILD_IMAGE),
+      },
+      {
+        inputs: [
+          expect.objectContaining({
+            label: 'Installation Steps',
+            target: VAR_INSTALLATION_STEPS,
+          }),
+        ],
+        template: expect.stringContaining(VAR_INSTALLATION_STEPS),
+      },
+      {
+        inputs: [
+          expect.objectContaining({
+            label: 'Build Steps',
+            target: VAR_BUILD_STEPS,
+          }),
+        ],
+        template: expect.stringContaining(VAR_BUILD_STEPS),
+      },
+    ]);
+  });
+
+  it('addresses all relevant instructions for a pages pipeline', () => {
+    const fullTemplate = getFinalTemplate();
+
+    expect(fullTemplate.toString()).toEqual(
+      `# The Docker image that will be used to build your app
+image: ${VAR_BUILD_IMAGE}
+# Functions that should be executed before the build script is run
+before_script: ${VAR_INSTALLATION_STEPS}
+pages:
+  script: ${VAR_BUILD_STEPS}
+  artifacts:
+    paths:
+      # The folder that contains the files to be exposed at the Page URL
+      - public
+  rules:
+    # This ensures that only pushes to the default branch will trigger
+    # a pages deploy
+    - if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH
+`,
+    );
+  });
+});
diff --git a/spec/graphql/mutations/pages/mark_onboarding_complete_spec.rb b/spec/graphql/mutations/pages/mark_onboarding_complete_spec.rb
new file mode 100644
index 000000000000..c4ceecb9d46f
--- /dev/null
+++ b/spec/graphql/mutations/pages/mark_onboarding_complete_spec.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Pages::MarkOnboardingComplete do
+  let_it_be(:project) { create(:project, :public, :repository) }
+  let_it_be(:developer) { create(:user) }
+  let_it_be(:owner) { create(:user) }
+
+  let(:mutation) { described_class.new(object: nil, context: { current_user: current_user }, field: nil) }
+
+  let(:mutation_arguments) do
+    {
+      project_path: project.full_path
+    }
+  end
+
+  before_all do
+    project.add_owner(owner)
+    project.add_developer(developer)
+  end
+
+  describe '#resolve' do
+    subject(:resolve) do
+      mutation.resolve(**mutation_arguments)
+    end
+
+    context 'when the current user has access to update pages' do
+      let(:current_user) { owner }
+
+      it 'calls mark_pages_onboarding_complete on the project' do
+        allow_next_instance_of(::Project) do |project|
+          expect(project).to receive(:mark_pages_onboarding_complete)
+        end
+      end
+
+      it 'returns onboarding_complete state' do
+        expect(resolve).to include(onboarding_complete: true)
+      end
+
+      it 'returns no errors' do
+        expect(resolve).to include(errors: [])
+      end
+    end
+
+    context "when the current user doesn't have access to update pages" do
+      let(:current_user) { developer }
+
+      it 'raises an error' do
+        expect { subject }.to raise_error(
+          Gitlab::Graphql::Errors::ResourceNotAvailable,
+          Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR
+        )
+      end
+    end
+  end
+end
-- 
GitLab