diff options
127 files changed, 2791 insertions, 1868 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 5d1508b5ea8..2566b978bc5 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -46f08adbf4930f6a9c56f37ef5e4106c5b50810f +76c3ec82133c6c2faea451440f2bd491dda4e94f diff --git a/app/assets/javascripts/error_tracking_settings/components/app.vue b/app/assets/javascripts/error_tracking_settings/components/app.vue index 971eb21ee3b..d188574e721 100644 --- a/app/assets/javascripts/error_tracking_settings/components/app.vue +++ b/app/assets/javascripts/error_tracking_settings/components/app.vue @@ -1,11 +1,17 @@ <script> -import { GlButton } from '@gitlab/ui'; +import { GlButton, GlFormGroup, GlFormCheckbox } from '@gitlab/ui'; import { mapActions, mapGetters, mapState } from 'vuex'; import ErrorTrackingForm from './error_tracking_form.vue'; import ProjectDropdown from './project_dropdown.vue'; export default { - components: { ProjectDropdown, ErrorTrackingForm, GlButton }, + components: { + ErrorTrackingForm, + GlButton, + GlFormCheckbox, + GlFormGroup, + ProjectDropdown, + }, props: { initialApiHost: { type: String, @@ -66,18 +72,18 @@ export default { <template> <div> - <div class="form-check form-group"> - <input + <gl-form-group + :label="s__('ErrorTracking|Enable error tracking')" + label-for="error-tracking-enabled" + > + <gl-form-checkbox id="error-tracking-enabled" :checked="enabled" - class="form-check-input" - type="checkbox" - @change="updateEnabled($event.target.checked)" - /> - <label class="form-check-label" for="error-tracking-enabled">{{ - s__('ErrorTracking|Active') - }}</label> - </div> + @change="updateEnabled($event)" + > + {{ s__('ErrorTracking|Active') }} + </gl-form-checkbox> + </gl-form-group> <error-tracking-form /> <div class="form-group"> <project-dropdown @@ -95,7 +101,7 @@ export default { <gl-button :disabled="settingsLoading" class="js-error-tracking-button" - variant="success" + variant="confirm" @click="handleSubmit" > {{ __('Save changes') }} diff --git a/app/assets/javascripts/error_tracking_settings/components/error_tracking_form.vue b/app/assets/javascripts/error_tracking_settings/components/error_tracking_form.vue index 4df324b396c..da942dbd0ae 100644 --- a/app/assets/javascripts/error_tracking_settings/components/error_tracking_form.vue +++ b/app/assets/javascripts/error_tracking_settings/components/error_tracking_form.vue @@ -70,7 +70,7 @@ export default { v-show="connectSuccessful" class="js-error-tracking-connect-success gl-ml-2 text-success align-middle" :aria-label="__('Projects Successfully Retrieved')" - name="check-circle" + name="check" /> </div> </div> diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue b/app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue index aed879e75fb..1d36b370457 100644 --- a/app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue +++ b/app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue @@ -72,11 +72,11 @@ export default { }, isAlreadyImported() { - return this.group.status !== STATUSES.NONE; + return this.group.progress.status !== STATUSES.NONE; }, isFinished() { - return this.group.status === STATUSES.FINISHED; + return this.group.progress.status === STATUSES.FINISHED; }, fullPath() { @@ -165,7 +165,7 @@ export default { </div> </td> <td class="gl-p-4 gl-white-space-nowrap"> - <import-status :status="group.status" /> + <import-status :status="group.progress.status" /> </td> <td class="gl-p-4"> <gl-button diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js b/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js index d444cc77aa7..facefe316eb 100644 --- a/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js +++ b/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js @@ -4,40 +4,82 @@ import axios from '~/lib/utils/axios_utils'; import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils'; import { s__ } from '~/locale'; import { STATUSES } from '../../constants'; +import bulkImportSourceGroupItemFragment from './fragments/bulk_import_source_group_item.fragment.graphql'; +import setImportProgressMutation from './mutations/set_import_progress.mutation.graphql'; +import updateImportStatusMutation from './mutations/update_import_status.mutation.graphql'; import availableNamespacesQuery from './queries/available_namespaces.query.graphql'; +import bulkImportSourceGroupQuery from './queries/bulk_import_source_group.query.graphql'; import { SourceGroupsManager } from './services/source_groups_manager'; import { StatusPoller } from './services/status_poller'; +import typeDefs from './typedefs.graphql'; export const clientTypenames = { BulkImportSourceGroupConnection: 'ClientBulkImportSourceGroupConnection', BulkImportSourceGroup: 'ClientBulkImportSourceGroup', AvailableNamespace: 'ClientAvailableNamespace', BulkImportPageInfo: 'ClientBulkImportPageInfo', + BulkImportTarget: 'ClientBulkImportTarget', + BulkImportProgress: 'ClientBulkImportProgress', }; -export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGroupsManager }) { - let statusPoller; +function makeGroup(data) { + const result = { + __typename: clientTypenames.BulkImportSourceGroup, + ...data, + }; + const NESTED_OBJECT_FIELDS = { + import_target: clientTypenames.BulkImportTarget, + progress: clientTypenames.BulkImportProgress, + }; - let sourceGroupManager; - const getGroupsManager = (client) => { - if (!sourceGroupManager) { - sourceGroupManager = new GroupsManager({ client, sourceUrl }); + Object.entries(NESTED_OBJECT_FIELDS).forEach(([field, type]) => { + if (!data[field]) { + return; } - return sourceGroupManager; - }; + result[field] = { + __typename: type, + ...data[field], + }; + }); + + return result; +} + +const localProgressId = (id) => `not-started-${id}`; + +export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGroupsManager }) { + const groupsManager = new GroupsManager({ + sourceUrl, + }); + + let statusPoller; return { Query: { + async bulkImportSourceGroup(_, { id }, { client, getCacheKey }) { + return client.readFragment({ + fragment: bulkImportSourceGroupItemFragment, + fragmentName: 'BulkImportSourceGroupItem', + id: getCacheKey({ + __typename: clientTypenames.BulkImportSourceGroup, + id, + }), + }); + }, + async bulkImportSourceGroups(_, vars, { client }) { if (!statusPoller) { statusPoller = new StatusPoller({ - groupManager: getGroupsManager(client), + updateImportStatus: ({ id, status_name: status }) => + client.mutate({ + mutation: updateImportStatusMutation, + variables: { id, status }, + }), pollPath: endpoints.jobs, }); statusPoller.startPolling(); } - const groupsManager = getGroupsManager(client); return Promise.all([ axios.get(endpoints.status, { params: { @@ -59,19 +101,20 @@ export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGr return { __typename: clientTypenames.BulkImportSourceGroupConnection, nodes: data.importable_data.map((group) => { - const cachedImportState = groupsManager.getImportStateFromStorageByGroupId( - group.id, - ); + const { jobId, importState: cachedImportState } = + groupsManager.getImportStateFromStorageByGroupId(group.id) ?? {}; - return { - __typename: clientTypenames.BulkImportSourceGroup, + return makeGroup({ ...group, - status: cachedImportState?.status ?? STATUSES.NONE, + progress: { + id: jobId ?? localProgressId(group.id), + status: cachedImportState?.status ?? STATUSES.NONE, + }, import_target: cachedImportState?.importTarget ?? { new_name: group.full_path, target_namespace: availableNamespaces[0]?.full_path ?? '', }, - }; + }); }), pageInfo: { __typename: clientTypenames.BulkImportPageInfo, @@ -91,26 +134,65 @@ export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGr ), }, Mutation: { - setTargetNamespace(_, { targetNamespace, sourceGroupId }, { client }) { - getGroupsManager(client).updateById(sourceGroupId, (sourceGroup) => { - // eslint-disable-next-line no-param-reassign - sourceGroup.import_target.target_namespace = targetNamespace; + setTargetNamespace: (_, { targetNamespace, sourceGroupId }) => + makeGroup({ + id: sourceGroupId, + import_target: { + target_namespace: targetNamespace, + }, + }), + + setNewName: (_, { newName, sourceGroupId }) => + makeGroup({ + id: sourceGroupId, + import_target: { + new_name: newName, + }, + }), + + async setImportProgress(_, { sourceGroupId, status, jobId }) { + if (jobId) { + groupsManager.saveImportState(jobId, { status }); + } + + return makeGroup({ + id: sourceGroupId, + progress: { + id: jobId ?? localProgressId(sourceGroupId), + status, + }, }); }, - setNewName(_, { newName, sourceGroupId }, { client }) { - getGroupsManager(client).updateById(sourceGroupId, (sourceGroup) => { - // eslint-disable-next-line no-param-reassign - sourceGroup.import_target.new_name = newName; - }); + async updateImportStatus(_, { id, status }) { + groupsManager.saveImportState(id, { status }); + + return { + __typename: clientTypenames.BulkImportProgress, + id, + status, + }; }, async importGroup(_, { sourceGroupId }, { client }) { - const groupManager = getGroupsManager(client); - const group = groupManager.findById(sourceGroupId); - groupManager.setImportStatus(group, STATUSES.SCHEDULING); - try { - const response = await axios.post(endpoints.createBulkImport, { + const { + data: { bulkImportSourceGroup: group }, + } = await client.query({ + query: bulkImportSourceGroupQuery, + variables: { id: sourceGroupId }, + }); + + const GROUP_BEING_SCHEDULED = makeGroup({ + id: sourceGroupId, + progress: { + id: localProgressId(sourceGroupId), + status: STATUSES.SCHEDULING, + }, + }); + + const defaultErrorMessage = s__('BulkImport|Importing the group failed'); + axios + .post(endpoints.createBulkImport, { bulk_import: [ { source_type: 'group_entity', @@ -119,18 +201,38 @@ export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGr destination_name: group.import_target.new_name, }, ], - }); - groupManager.startImport({ group, importId: response.data.id }); - } catch (e) { - const message = e?.response?.data?.error ?? s__('BulkImport|Importing the group failed'); - createFlash({ message }); - groupManager.setImportStatus(group, STATUSES.NONE); - throw e; - } + }) + .then(({ data: { id: jobId } }) => { + groupsManager.saveImportState(jobId, { + id: group.id, + importTarget: group.import_target, + status: STATUSES.CREATED, + }); + + return { status: STATUSES.CREATED, jobId }; + }) + .catch((e) => { + const message = e?.response?.data?.error ?? defaultErrorMessage; + createFlash({ message }); + return { status: STATUSES.NONE }; + }) + .then((newStatus) => + client.mutate({ + mutation: setImportProgressMutation, + variables: { sourceGroupId, ...newStatus }, + }), + ) + .catch(() => createFlash({ message: defaultErrorMessage })); + + return GROUP_BEING_SCHEDULED; }, }, }; } export const createApolloClient = ({ sourceUrl, endpoints }) => - createDefaultClient(createResolvers({ sourceUrl, endpoints }), { assumeImmutableResults: true }); + createDefaultClient( + createResolvers({ sourceUrl, endpoints }), + { assumeImmutableResults: true }, + typeDefs, + ); diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_item.fragment.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_item.fragment.graphql index 50774e36599..ee3add7966c 100644 --- a/app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_item.fragment.graphql +++ b/app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_item.fragment.graphql @@ -1,8 +1,15 @@ +#import "./bulk_import_source_group_progress.fragment.graphql" + fragment BulkImportSourceGroupItem on ClientBulkImportSourceGroup { id web_url full_path full_name - status - import_target + progress { + ...BulkImportSourceGroupProgress + } + import_target { + target_namespace + new_name + } } diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_progress.fragment.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_progress.fragment.graphql new file mode 100644 index 00000000000..2d60bf82d65 --- /dev/null +++ b/app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_progress.fragment.graphql @@ -0,0 +1,4 @@ +fragment BulkImportSourceGroupProgress on ClientBulkImportProgress { + id + status +} diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_group.mutation.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_group.mutation.graphql index 412608d3faf..41d32a1d639 100644 --- a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_group.mutation.graphql +++ b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_group.mutation.graphql @@ -1,3 +1,9 @@ mutation importGroup($sourceGroupId: String!) { - importGroup(sourceGroupId: $sourceGroupId) @client + importGroup(sourceGroupId: $sourceGroupId) @client { + id + progress { + id + status + } + } } diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_import_progress.mutation.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_import_progress.mutation.graphql new file mode 100644 index 00000000000..2ec1269932a --- /dev/null +++ b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_import_progress.mutation.graphql @@ -0,0 +1,9 @@ +mutation setImportProgress($status: String!, $sourceGroupId: String!, $jobId: String) { + setImportProgress(status: $status, sourceGroupId: $sourceGroupId, jobId: $jobId) @client { + id + progress { + id + status + } + } +} diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql index 2bc19891401..354bf2a5815 100644 --- a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql +++ b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql @@ -1,3 +1,8 @@ mutation setNewName($newName: String!, $sourceGroupId: String!) { - setNewName(newName: $newName, sourceGroupId: $sourceGroupId) @client + setNewName(newName: $newName, sourceGroupId: $sourceGroupId) @client { + id + import_target { + new_name + } + } } diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql index fc98a1652c1..a0ef407f135 100644 --- a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql +++ b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql @@ -1,3 +1,8 @@ mutation setTargetNamespace($targetNamespace: String!, $sourceGroupId: String!) { - setTargetNamespace(targetNamespace: $targetNamespace, sourceGroupId: $sourceGroupId) @client + setTargetNamespace(targetNamespace: $targetNamespace, sourceGroupId: $sourceGroupId) @client { + id + import_target { + target_namespace + } + } } diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/update_import_status.mutation.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/update_import_status.mutation.graphql new file mode 100644 index 00000000000..8c0233b2939 --- /dev/null +++ b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/update_import_status.mutation.graphql @@ -0,0 +1,6 @@ +mutation updateImportStatus($status: String!, $id: String!) { + updateImportStatus(status: $status, id: $id) @client { + id + status + } +} diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/queries/bulk_import_source_group.query.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/queries/bulk_import_source_group.query.graphql new file mode 100644 index 00000000000..0aff23af96d --- /dev/null +++ b/app/assets/javascripts/import_entities/import_groups/graphql/queries/bulk_import_source_group.query.graphql @@ -0,0 +1,7 @@ +#import "../fragments/bulk_import_source_group_item.fragment.graphql" + +query bulkImportSourceGroup($id: ID!) { + bulkImportSourceGroup(id: $id) @client { + ...BulkImportSourceGroupItem + } +} diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js b/app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js index 2c88d25358f..536f96529d7 100644 --- a/app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js +++ b/app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js @@ -1,26 +1,10 @@ -import { defaultDataIdFromObject } from 'apollo-cache-inmemory'; -import produce from 'immer'; import { debounce, merge } from 'lodash'; -import { STATUSES } from '../../../constants'; -import ImportSourceGroupFragment from '../fragments/bulk_import_source_group_item.fragment.graphql'; - -function extractTypeConditionFromFragment(fragment) { - return fragment.definitions[0]?.typeCondition.name.value; -} - -function generateGroupId(id) { - return defaultDataIdFromObject({ - __typename: extractTypeConditionFromFragment(ImportSourceGroupFragment), - id, - }); -} export const KEY = 'gl-bulk-imports-import-state'; export const DEBOUNCE_INTERVAL = 200; export class SourceGroupsManager { - constructor({ client, sourceUrl, storage = window.localStorage }) { - this.client = client; + constructor({ sourceUrl, storage = window.localStorage }) { this.sourceUrl = sourceUrl; this.storage = storage; @@ -35,45 +19,30 @@ export class SourceGroupsManager { } } - findById(id) { - const cacheId = generateGroupId(id); - return this.client.readFragment({ fragment: ImportSourceGroupFragment, id: cacheId }); - } - - update(group, fn) { - this.client.writeFragment({ - fragment: ImportSourceGroupFragment, - id: generateGroupId(group.id), - data: produce(group, fn), - }); - } + saveImportState(importId, group) { + const key = this.getStorageKey(importId); + const oldState = this.importStates[key] ?? {}; - updateById(id, fn) { - const group = this.findById(id); - this.update(group, fn); - } + if (!oldState.id && !group.id) { + return; + } - saveImportState(importId, group) { - this.importStates[this.getStorageKey(importId)] = { - id: group.id, - importTarget: group.import_target, + this.importStates[key] = { + ...oldState, + ...group, status: group.status, }; this.saveImportStatesToStorage(); } - getImportStateFromStorage(importId) { - return this.importStates[this.getStorageKey(importId)]; - } - getImportStateFromStorageByGroupId(groupId) { const PREFIX = this.getStorageKey(''); - const [, importState] = + const [jobId, importState] = Object.entries(this.importStates).find( ([key, group]) => key.startsWith(PREFIX) && group.id === groupId, ) ?? []; - return importState; + return { jobId, importState }; } getStorageKey(importId) { @@ -91,34 +60,4 @@ export class SourceGroupsManager { // empty catch intentional: storage might be unavailable or full } }, DEBOUNCE_INTERVAL); - - startImport({ group, importId }) { - this.setImportStatus(group, STATUSES.CREATED); - this.saveImportState(importId, group); - } - - setImportStatus(group, status) { - this.update(group, (sourceGroup) => { - // eslint-disable-next-line no-param-reassign - sourceGroup.status = status; - }); - } - - setImportStatusByImportId(importId, status) { - const importState = this.getImportStateFromStorage(importId); - if (!importState) { - return; - } - - if (importState.status !== status) { - importState.status = status; - } - - const group = this.findById(importState.id); - if (group?.id) { - this.setImportStatus(group, status); - } - - this.saveImportStatesToStorage(); - } } diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js b/app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js index b80a575afce..0297b3d3428 100644 --- a/app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js +++ b/app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js @@ -5,13 +5,15 @@ import Poll from '~/lib/utils/poll'; import { s__ } from '~/locale'; export class StatusPoller { - constructor({ groupManager, pollPath }) { + constructor({ updateImportStatus, pollPath }) { this.eTagPoll = new Poll({ resource: { fetchJobs: () => axios.get(pollPath), }, method: 'fetchJobs', - successCallback: ({ data }) => this.updateImportsStatuses(data), + successCallback: ({ data: statuses }) => { + statuses.forEach((status) => updateImportStatus(status)); + }, errorCallback: () => createFlash({ message: s__('BulkImport|Update of import statuses with realtime changes failed'), @@ -25,17 +27,9 @@ export class StatusPoller { this.eTagPoll.stop(); } }); - - this.groupManager = groupManager; } startPolling() { this.eTagPoll.makeRequest(); } - - async updateImportsStatuses(importStatuses) { - importStatuses.forEach(({ id, status_name: statusName }) => { - this.groupManager.setImportStatusByImportId(id, statusName); - }); - } } diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/typedefs.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/typedefs.graphql new file mode 100644 index 00000000000..ba042d33893 --- /dev/null +++ b/app/assets/javascripts/import_entities/import_groups/graphql/typedefs.graphql @@ -0,0 +1,53 @@ +type ClientBulkImportAvailableNamespace { + id: ID! + full_path: String! +} + +type ClientBulkImportTarget { + target_namespace: String! + new_name: String! +} + +type ClientBulkImportSourceGroupConnection { + nodes: [ClientBulkImportSourceGroup!]! + pageInfo: ClientBulkImportPageInfo! +} + +type ClientBulkImportProgress { + id: ID + status: String! +} + +type ClientBulkImportSourceGroup { + id: ID! + web_url: String! + full_path: String! + full_name: String! + progress: ClientBulkImportProgress! + import_target: ClientBulkImportTarget! +} + +type ClientBulkImportPageInfo { + page: Int! + perPage: Int! + total: Int! + totalPages: Int! +} + +extend type Query { + bulkImportSourceGroup(id: ID!): ClientBulkImportSourceGroup + bulkImportSourceGroups( + page: Int! + perPage: Int! + filter: String! + ): ClientBulkImportSourceGroupConnection! + availableNamespaces: [ClientBulkImportAvailableNamespace!]! +} + +extend type Mutation { + setNewName(newName: String, sourceGroupId: ID!): ClientTargetNamespace! + setTargetNamespace(targetNamespace: String, sourceGroupId: ID!): ClientTargetNamespace! + importGroup(id: ID!): ClientBulkImportSourceGroup! + setImportProgress(id: ID, status: String!): ClientBulkImportSourceGroup! + updateImportProgress(id: ID, status: String!): ClientBulkImportProgress +} diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue index cced8462955..0d7c2d526ca 100644 --- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue +++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue @@ -1,19 +1,17 @@ <script> -import { GlDropdownItem, GlDropdownDivider, GlSearchBoxByType, GlLoadingIcon } from '@gitlab/ui'; +import { GlDropdownItem } 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 SidebarAssigneesRealtime from '~/sidebar/components/assignees/assignees_realtime.vue'; import IssuableAssignees from '~/sidebar/components/assignees/issuable_assignees.vue'; import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue'; -import { assigneesQueries, ASSIGNEES_DEBOUNCE_DELAY } from '~/sidebar/constants'; -import MultiSelectDropdown from '~/vue_shared/components/sidebar/multiselect_dropdown.vue'; +import { assigneesQueries } from '~/sidebar/constants'; +import UserSelect from '~/vue_shared/components/user_select/user_select.vue'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import SidebarInviteMembers from './sidebar_invite_members.vue'; -import SidebarParticipant from './sidebar_participant.vue'; export const assigneesWidget = Vue.observable({ updateAssignees: null, @@ -33,14 +31,10 @@ export default { components: { SidebarEditableItem, IssuableAssignees, - MultiSelectDropdown, GlDropdownItem, - GlDropdownDivider, - GlSearchBoxByType, - GlLoadingIcon, SidebarInviteMembers, - SidebarParticipant, SidebarAssigneesRealtime, + UserSelect, }, mixins: [glFeatureFlagsMixin()], inject: { @@ -75,7 +69,7 @@ export default { required: false, default: null, }, - multipleAssignees: { + allowMultipleAssignees: { type: Boolean, required: false, default: true, @@ -83,12 +77,9 @@ export default { }, data() { return { - search: '', issuable: {}, - searchUsers: [], selected: [], isSettingAssignees: false, - isSearching: false, isDirty: false, }; }, @@ -106,51 +97,13 @@ export default { result({ data }) { const issuable = data.workspace?.issuable; if (issuable) { - this.selected = this.moveCurrentUserToStart(cloneDeep(issuable.assignees.nodes)); + this.selected = cloneDeep(issuable.assignees.nodes); } }, error() { createFlash({ message: __('An error occurred while fetching participants.') }); }, }, - searchUsers: { - query: searchUsers, - variables() { - return { - fullPath: this.fullPath, - search: this.search, - }; - }, - update(data) { - const searchResults = data.workspace?.users?.nodes.map(({ user }) => user) || []; - const filteredParticipants = this.participants.filter( - (user) => - user.name.toLowerCase().includes(this.search.toLowerCase()) || - user.username.toLowerCase().includes(this.search.toLowerCase()), - ); - const mergedSearchResults = searchResults.reduce((acc, current) => { - // Some users are duplicated in the query result: - // https://gitlab.com/gitlab-org/gitlab/-/issues/327822 - if (!acc.some((user) => current.username === user.username)) { - acc.push(current); - } - return acc; - }, filteredParticipants); - - return mergedSearchResults; - }, - debounce: ASSIGNEES_DEBOUNCE_DELAY, - skip() { - return this.isSearchEmpty; - }, - error() { - createFlash({ message: __('An error occurred while searching users.') }); - this.isSearching = false; - }, - result() { - this.isSearching = false; - }, - }, }, computed: { shouldEnableRealtime() { @@ -169,13 +122,6 @@ export default { : 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; if (!items) { @@ -183,28 +129,8 @@ export default { } 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 === ''; + isAssigneesLoading() { + return !this.initialAssignees && this.$apollo.queries.issuable.loading; }, currentUser() { return { @@ -213,35 +139,9 @@ export default { 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.searchUsers.length === 0; - }, signedIn() { return this.currentUser.username !== undefined; }, - showCurrentUser() { - return ( - this.signedIn && - !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; @@ -271,68 +171,31 @@ export default { this.isSettingAssignees = false; }); }, - selectAssignee(name) { - this.isDirty = true; - - if (!this.multipleAssignees) { - this.selected = name ? [name] : []; - this.collapseWidget(); - return; - } - if (name === undefined) { - this.clearSelected(); - return; - } - this.selected = this.selected.concat(name); - }, - unselect(name) { - this.selected = this.selected.filter((user) => user.username !== name); - this.isDirty = true; - - if (!this.multipleAssignees) { - this.collapseWidget(); - } - }, assignSelf() { - this.updateAssignees(this.currentUser.username); - }, - clearSelected() { - this.selected = []; + this.updateAssignees([this.currentUser.username]); }, saveAssignees() { this.isDirty = false; - this.updateAssignees(this.selectedUserNames); + this.updateAssignees(this.selected.map(({ username }) => username)); this.$el.dispatchEvent(hideDropdownEvent); }, - 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(); }, expandWidget() { this.$refs.toggle.expand(); }, - showDivider(list) { - return list.length > 0 && this.isSearchEmpty; + focusSearch() { + this.$refs.userSelect.focusSearch(); + }, + showError() { + createFlash({ message: __('An error occurred while fetching participants.') }); + }, + setDirtyState() { + this.isDirty = true; + if (!this.allowMultipleAssignees) { + this.collapseWidget(); + } }, }, }; @@ -365,86 +228,27 @@ export default { @expand-widget="expandWidget" /> </template> - <template #default> - <multi-select-dropdown - class="gl-w-full dropdown-menu-user" + <user-select + ref="userSelect" + v-model="selected" :text="$options.i18n.assignees" :header-text="$options.i18n.assignTo" + :iid="iid" + :full-path="fullPath" + :allow-multiple-assignees="allowMultipleAssignees" + :current-user="currentUser" + :issuable-type="issuableType" + class="gl-w-full dropdown-menu-user" @toggle="collapseWidget" + @error="showError" + @input="setDirtyState" > - <template #search> - <gl-search-box-by-type - ref="search" - v-model.trim="search" - class="js-dropdown-input-field" - /> - </template> - <template #items> - <gl-loading-icon - v-if="$apollo.queries.searchUsers.loading || $apollo.queries.issuable.loading" - data-testid="loading-participants" - size="lg" - /> - <template v-else> - <template v-if="isSearchEmpty || isSearching"> - <gl-dropdown-item - :is-checked="selectedIsEmpty" - :is-check-centered="true" - data-testid="unassign" - @click="selectAssignee()" - > - <span - :class="selectedIsEmpty ? 'gl-pl-0' : 'gl-pl-6'" - class="gl-font-weight-bold" - >{{ $options.i18n.unassigned }}</span - ></gl-dropdown-item - > - </template> - <gl-dropdown-divider v-if="showDivider(selectedFiltered)" /> - <gl-dropdown-item - v-for="item in selectedFiltered" - :key="item.id" - :is-checked="isChecked(item.username)" - :is-check-centered="true" - data-testid="selected-participant" - @click.stop="unselect(item.username)" - > - <sidebar-participant :user="item" /> - </gl-dropdown-item> - <template v-if="showCurrentUser"> - <gl-dropdown-divider /> - <gl-dropdown-item - data-testid="current-user" - @click.stop="selectAssignee(currentUser)" - > - <sidebar-participant :user="currentUser" class="gl-pl-6!" /> - </gl-dropdown-item> - </template> - <gl-dropdown-divider v-if="showDivider(unselectedFiltered)" /> - <gl-dropdown-item - v-for="unselectedUser in unselectedFiltered" - :key="unselectedUser.id" - data-testid="unselected-participant" - @click="selectAssignee(unselectedUser)" - > - <sidebar-participant :user="unselectedUser" class="gl-pl-6!" /> - </gl-dropdown-item> - <gl-dropdown-item - v-if="noUsersFound && !isSearching" - data-testid="empty-results" - class="gl-pl-6!" - > - {{ __('No matching results') }} - </gl-dropdown-item> - </template> - </template> <template #footer> <gl-dropdown-item> <sidebar-invite-members v-if="directlyInviteMembers" /> - </gl-dropdown-item> - </template> - </multi-select-dropdown> + </gl-dropdown-item> </template + ></user-select> </template> </sidebar-editable-item> </div> diff --git a/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue b/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue index 809dd1b2dba..148ecae0785 100644 --- a/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue +++ b/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue @@ -3,6 +3,9 @@ import { GlButton, GlLoadingIcon } from '@gitlab/ui'; import { __ } from '~/locale'; export default { + i18n: { + unassigned: __('Unassigned'), + }, components: { GlButton, GlLoadingIcon }, inject: { canUpdate: {}, diff --git a/app/assets/javascripts/sidebar/constants.js b/app/assets/javascripts/sidebar/constants.js index 82cad094d6b..21876ee8dab 100644 --- a/app/assets/javascripts/sidebar/constants.js +++ b/app/assets/javascripts/sidebar/constants.js @@ -12,22 +12,33 @@ import updateEpicDueDateMutation from '~/sidebar/queries/update_epic_due_date.mu import updateEpicStartDateMutation from '~/sidebar/queries/update_epic_start_date.mutation.graphql'; import updateIssueConfidentialMutation from '~/sidebar/queries/update_issue_confidential.mutation.graphql'; import updateIssueDueDateMutation from '~/sidebar/queries/update_issue_due_date.mutation.graphql'; -import getIssueParticipants from '~/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql'; +import getIssueAssignees from '~/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql'; +import issueParticipantsQuery from '~/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql'; +import getMergeRequestAssignees from '~/vue_shared/components/sidebar/queries/get_mr_assignees.query.graphql'; import getMergeRequestParticipants from '~/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql'; -import updateAssigneesMutation from '~/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql'; -import updateMergeRequestParticipantsMutation from '~/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql'; +import updateIssueAssigneesMutation from '~/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql'; +import updateMergeRequestAssigneesMutation from '~/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql'; export const ASSIGNEES_DEBOUNCE_DELAY = 250; export const assigneesQueries = { [IssuableType.Issue]: { - query: getIssueParticipants, + query: getIssueAssignees, subscription: issuableAssigneesSubscription, - mutation: updateAssigneesMutation, + mutation: updateIssueAssigneesMutation, + }, + [IssuableType.MergeRequest]: { + query: getMergeRequestAssignees, + mutation: updateMergeRequestAssigneesMutation, + }, +}; + +export const participantsQueries = { + [IssuableType.Issue]: { + query: issueParticipantsQuery, }, [IssuableType.MergeRequest]: { query: getMergeRequestParticipants, - mutation: updateMergeRequestParticipantsMutation, }, }; diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js index b1d35a1e6f4..a214dcd4479 100644 --- a/app/assets/javascripts/sidebar/mount_sidebar.js +++ b/app/assets/javascripts/sidebar/mount_sidebar.js @@ -108,7 +108,7 @@ function mountAssigneesComponent() { ? IssuableType.Issue : IssuableType.MergeRequest, issuableId: id, - multipleAssignees: !el.dataset.maxAssignees, + allowMultipleAssignees: !el.dataset.maxAssignees, }, scopedSlots: { collapsed: ({ users, onClick }) => diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue index 8fa6931ac00..32749b8b018 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue @@ -1,4 +1,5 @@ <script> +import { MERGE_ACTIVE_STATUS_PHRASES } from '../../constants'; import statusIcon from '../mr_widget_status_icon.vue'; export default { @@ -13,6 +14,13 @@ export default { default: () => ({}), }, }, + data() { + const statusCount = MERGE_ACTIVE_STATUS_PHRASES.length; + + return { + mergeStatus: MERGE_ACTIVE_STATUS_PHRASES[Math.floor(Math.random() * statusCount)], + }; + }, }; </script> <template> @@ -20,8 +28,8 @@ export default { <status-icon status="loading" /> <div class="media-body"> <h4> - {{ s__('mrWidget|Merging! Drum roll, please…') }} - <gl-emoji data-name="drum" /> + {{ mergeStatus.message }} + <gl-emoji :data-name="mergeStatus.emoji" /> </h4> <section class="mr-info-list"> <p> diff --git a/app/assets/javascripts/vue_merge_request_widget/constants.js b/app/assets/javascripts/vue_merge_request_widget/constants.js index 77dfbf9d385..822fb58db60 100644 --- a/app/assets/javascripts/vue_merge_request_widget/constants.js +++ b/app/assets/javascripts/vue_merge_request_widget/constants.js @@ -25,3 +25,30 @@ export const SP_HELP_CONTENT = s__( ); export const SP_HELP_URL = 'https://about.gitlab.com/blog/2019/07/12/guide-to-ci-cd-pipelines/'; export const SP_ICON_NAME = 'status_notfound'; + +export const MERGE_ACTIVE_STATUS_PHRASES = [ + { + message: s__('mrWidget|Merging! Drum roll, please…'), + emoji: 'drum', + }, + { + message: s__("mrWidget|Merging! We're almost there…"), + emoji: 'sparkles', + }, + { + message: s__('mrWidget|Merging! Changes will land soon…'), + emoji: 'airplane_arriving', + }, + { + message: s__('mrWidget|Merging! Changes are being shipped…'), + emoji: 'ship', + }, + { + message: s__("mrWidget|Merging! Everything's good…"), + emoji: 'relieved', + }, + { + message: s__('mrWidget|Merging! This is going to be great…'), + emoji: 'heart_eyes', + }, +]; diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql new file mode 100644 index 00000000000..e3501e53bdc --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql @@ -0,0 +1,18 @@ +#import "~/graphql_shared/fragments/user.fragment.graphql" +#import "~/graphql_shared/fragments/user_availability.fragment.graphql" + +query issueParticipants($fullPath: ID!, $iid: String!) { + workspace: project(fullPath: $fullPath) { + __typename + issuable: issue(iid: $iid) { + __typename + id + assignees { + nodes { + ...User + ...UserAvailability + } + } + } + } +} diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql index 3885127fa8e..48787305459 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql +++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql @@ -13,12 +13,6 @@ query issueParticipants($fullPath: ID!, $iid: String!) { ...UserAvailability } } - assignees { - nodes { - ...User - ...UserAvailability - } - } } } } diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_assignees.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_assignees.query.graphql new file mode 100644 index 00000000000..53f7381760e --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_assignees.query.graphql @@ -0,0 +1,16 @@ +#import "~/graphql_shared/fragments/user.fragment.graphql" +#import "~/graphql_shared/fragments/user_availability.fragment.graphql" + +query getMrAssignees($fullPath: ID!, $iid: String!) { + workspace: project(fullPath: $fullPath) { + issuable: mergeRequest(iid: $iid) { + id + assignees { + nodes { + ...User + ...UserAvailability + } + } + } + } +} diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql index 63482873b69..6adbd4098f2 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql +++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql @@ -11,12 +11,6 @@ query getMrParticipants($fullPath: ID!, $iid: String!) { ...UserAvailability } } - assignees { - nodes { - ...User - ...UserAvailability - } - } } } } diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql index 3f40c0368d7..24de5ea4fe3 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql +++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql @@ -13,12 +13,6 @@ mutation issueSetAssignees($iid: String!, $assigneeUsernames: [String!]!, $fullP ...UserAvailability } } - participants { - nodes { - ...User - ...UserAvailability - } - } } } } diff --git a/app/assets/javascripts/vue_shared/components/user_select/user_select.vue b/app/assets/javascripts/vue_shared/components/user_select/user_select.vue new file mode 100644 index 00000000000..22b8d707de8 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/user_select/user_select.vue @@ -0,0 +1,276 @@ +<script> +import { + GlDropdown, + GlDropdownForm, + GlDropdownDivider, + GlDropdownItem, + GlSearchBoxByType, + GlLoadingIcon, +} from '@gitlab/ui'; +import searchUsers from '~/graphql_shared/queries/users_search.query.graphql'; +import { __ } from '~/locale'; +import SidebarParticipant from '~/sidebar/components/assignees/sidebar_participant.vue'; +import { ASSIGNEES_DEBOUNCE_DELAY, participantsQueries } from '~/sidebar/constants'; + +export default { + i18n: { + unassigned: __('Unassigned'), + }, + components: { + GlDropdownForm, + GlDropdown, + GlDropdownDivider, + GlDropdownItem, + GlSearchBoxByType, + SidebarParticipant, + GlLoadingIcon, + }, + props: { + headerText: { + type: String, + required: true, + }, + text: { + type: String, + required: true, + }, + fullPath: { + type: String, + required: true, + }, + iid: { + type: String, + required: true, + }, + value: { + type: Array, + required: true, + }, + allowMultipleAssignees: { + type: Boolean, + required: false, + default: false, + }, + currentUser: { + type: Object, + required: true, + }, + issuableType: { + type: String, + required: false, + default: 'issue', + }, + }, + data() { + return { + search: '', + participants: [], + searchUsers: [], + isSearching: false, + }; + }, + apollo: { + participants: { + query() { + return participantsQueries[this.issuableType].query; + }, + variables() { + return { + iid: this.iid, + fullPath: this.fullPath, + }; + }, + update(data) { + return data.workspace?.issuable?.participants.nodes; + }, + error() { + this.$emit('error'); + }, + }, + searchUsers: { + query: searchUsers, + variables() { + return { + fullPath: this.fullPath, + search: this.search, + first: 20, + }; + }, + update(data) { + return data.workspace?.users?.nodes.map(({ user }) => user) || []; + }, + debounce: ASSIGNEES_DEBOUNCE_DELAY, + error() { + this.$emit('error'); + this.isSearching = false; + }, + result() { + this.isSearching = false; + }, + }, + }, + computed: { + isLoading() { + return this.$apollo.queries.searchUsers.loading || this.$apollo.queries.participants.loading; + }, + users() { + if (!this.participants) { + return []; + } + const mergedSearchResults = this.participants.reduce((acc, current) => { + if ( + !acc.some((user) => current.username === user.username) && + (current.name.includes(this.search) || current.username.includes(this.search)) + ) { + acc.push(current); + } + return acc; + }, this.searchUsers); + return this.moveCurrentUserToStart(mergedSearchResults); + }, + isSearchEmpty() { + return this.search === ''; + }, + shouldShowParticipants() { + return this.isSearchEmpty || this.isSearching; + }, + isCurrentUserInList() { + const isCurrentUser = (user) => user.username === this.currentUser.username; + return this.users.some(isCurrentUser); + }, + noUsersFound() { + return !this.isSearchEmpty && this.users.length === 0; + }, + showCurrentUser() { + return this.currentUser.username && !this.isCurrentUserInList && this.isSearchEmpty; + }, + selectedFiltered() { + if (this.shouldShowParticipants) { + return this.moveCurrentUserToStart(this.value); + } + + const foundUsernames = this.users.map(({ username }) => username); + const filtered = this.value.filter(({ username }) => foundUsernames.includes(username)); + return this.moveCurrentUserToStart(filtered); + }, + selectedUserNames() { + return this.value.map(({ username }) => username); + }, + unselectedFiltered() { + return this.users?.filter(({ username }) => !this.selectedUserNames.includes(username)) || []; + }, + selectedIsEmpty() { + return this.selectedFiltered.length === 0; + }, + }, + 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; + } + }, + }, + methods: { + selectAssignee(user) { + let selected = [...this.value]; + if (!this.allowMultipleAssignees) { + selected = [user]; + } else { + selected.push(user); + } + this.$emit('input', selected); + }, + unselect(name) { + const selected = this.value.filter((user) => user.username !== name); + this.$emit('input', selected); + }, + focusSearch() { + this.$refs.search.focusInput(); + }, + showDivider(list) { + return list.length > 0 && this.isSearchEmpty; + }, + 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; + }, + }, +}; +</script> + +<template> + <gl-dropdown class="show" :text="text" @toggle="$emit('toggle')"> + <template #header> + <p class="gl-font-weight-bold gl-text-center gl-mt-2 gl-mb-4">{{ headerText }}</p> + <gl-dropdown-divider /> + <gl-search-box-by-type ref="search" v-model.trim="search" class="js-dropdown-input-field" /> + </template> + <gl-dropdown-form class="gl-relative gl-min-h-7"> + <gl-loading-icon + v-if="isLoading" + data-testid="loading-participants" + size="md" + class="gl-absolute gl-left-0 gl-top-0 gl-right-0" + /> + <template v-else> + <template v-if="shouldShowParticipants"> + <gl-dropdown-item + v-if="isSearchEmpty" + :is-checked="selectedIsEmpty" + :is-check-centered="true" + data-testid="unassign" + @click="$emit('input', [])" + > + <span :class="selectedIsEmpty ? 'gl-pl-0' : 'gl-pl-6'" class="gl-font-weight-bold">{{ + $options.i18n.unassigned + }}</span></gl-dropdown-item + > + </template> + <gl-dropdown-divider v-if="showDivider(selectedFiltered)" /> + <gl-dropdown-item + v-for="item in selectedFiltered" + :key="item.id" + is-checked + is-check-centered + data-testid="selected-participant" + @click.stop="unselect(item.username)" + > + <sidebar-participant :user="item" /> + </gl-dropdown-item> + <template v-if="showCurrentUser"> + <gl-dropdown-divider /> + <gl-dropdown-item data-testid="current-user" @click.stop="selectAssignee(currentUser)"> + <sidebar-participant :user="currentUser" class="gl-pl-6!" /> + </gl-dropdown-item> + </template> + <gl-dropdown-divider v-if="showDivider(unselectedFiltered)" /> + <gl-dropdown-item + v-for="unselectedUser in unselectedFiltered" + :key="unselectedUser.id" + data-testid="unselected-participant" + @click="selectAssignee(unselectedUser)" + > + <sidebar-participant :user="unselectedUser" class="gl-pl-6!" /> + </gl-dropdown-item> + <gl-dropdown-item v-if="noUsersFound" data-testid="empty-results" class="gl-pl-6!"> + {{ __('No matching results') }} + </gl-dropdown-item> + </template> + </gl-dropdown-form> + <template #footer> + <slot name="footer"></slot> + </template> + </gl-dropdown> +</template> diff --git a/app/controllers/registrations/experience_levels_controller.rb b/app/controllers/registrations/experience_levels_controller.rb index 3a721823d89..d04e8d296ed 100644 --- a/app/controllers/registrations/experience_levels_controller.rb +++ b/app/controllers/registrations/experience_levels_controller.rb @@ -38,7 +38,7 @@ module Registrations end def learn_gitlab - @learn_gitlab ||= LearnGitlab.new(current_user) + @learn_gitlab ||= LearnGitlab::Project.new(current_user) end end end diff --git a/app/helpers/learn_gitlab_helper.rb b/app/helpers/learn_gitlab_helper.rb index 4fff9990c29..fc03c986830 100644 --- a/app/helpers/learn_gitlab_helper.rb +++ b/app/helpers/learn_gitlab_helper.rb @@ -28,27 +28,13 @@ module LearnGitlabHelper private - ACTION_ISSUE_IDS = { - issue_created: 4, - git_write: 6, - pipeline_created: 7, - merge_request_created: 9, - user_added: 8, - trial_started: 2, - required_mr_approvals_enabled: 11, - code_owners_enabled: 10 - }.freeze - - ACTION_DOC_URLS = { - security_scan_enabled: 'https://docs.gitlab.com/ee/user/application_security/security_dashboard/#gitlab-security-dashboard-security-center-and-vulnerability-reports' - }.freeze - def action_urls - ACTION_ISSUE_IDS.transform_values { |id| project_issue_url(learn_gitlab_project, id) }.merge(ACTION_DOC_URLS) + LearnGitlab::Onboarding::ACTION_ISSUE_IDS.transform_values { |id| project_issue_url(learn_gitlab_project, id) } + .merge(LearnGitlab::Onboarding::ACTION_DOC_URLS) end def learn_gitlab_project - @learn_gitlab_project ||= LearnGitlab.new(current_user).project + @learn_gitlab_project ||= LearnGitlab::Project.new(current_user).project end def onboarding_progress(project) @@ -57,6 +43,6 @@ module LearnGitlabHelper def learn_gitlab_onboarding_available?(project) OnboardingProgress.onboarding?(project.namespace) && - LearnGitlab.new(current_user).available? + LearnGitlab::Project.new(current_user).available? end end diff --git a/app/models/bulk_imports/export.rb b/app/models/bulk_imports/export.rb new file mode 100644 index 00000000000..396180c5995 --- /dev/null +++ b/app/models/bulk_imports/export.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +module BulkImports + class Export < ApplicationRecord + include Gitlab::Utils::StrongMemoize + + self.table_name = 'bulk_import_exports' + + belongs_to :project, optional: true + belongs_to :group, optional: true + + has_one :upload, class_name: 'BulkImports::ExportUpload' + + validates :project, presence: true, unless: :group + validates :group, presence: true, unless: :project + validates :relation, :status, presence: true + + validate :exportable_relation? + + state_machine :status, initial: :started do + state :started, value: 0 + state :finished, value: 1 + state :failed, value: -1 + + event :start do + transition any => :started + end + + event :finish do + transition started: :finished + transition failed: :failed + end + + event :fail_op do + transition any => :failed + end + end + + def exportable_relation? + return unless exportable + + errors.add(:relation, 'Unsupported exportable relation') unless config.exportable_relations.include?(relation) + end + + def exportable + strong_memoize(:exportable) do + project || group + end + end + + def relation_definition + config.exportable_tree[:include].find { |include| include[relation.to_sym] } + end + + def config + strong_memoize(:config) do + case exportable + when ::Project + Exports::ProjectConfig.new(exportable) + when ::Group + Exports::GroupConfig.new(exportable) + end + end + end + end +end diff --git a/app/models/bulk_imports/export_upload.rb b/app/models/bulk_imports/export_upload.rb new file mode 100644 index 00000000000..a9cba5119af --- /dev/null +++ b/app/models/bulk_imports/export_upload.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module BulkImports + class ExportUpload < ApplicationRecord + include WithUploads + include ObjectStorage::BackgroundMove + + self.table_name = 'bulk_import_export_uploads' + + belongs_to :export, class_name: 'BulkImports::Export' + + mount_uploader :export_file, ExportUploader + + def retrieve_upload(_identifier, paths) + Upload.find_by(model: self, path: paths) + end + end +end diff --git a/app/models/bulk_imports/exports/base_config.rb b/app/models/bulk_imports/exports/base_config.rb new file mode 100644 index 00000000000..3e6792f07c5 --- /dev/null +++ b/app/models/bulk_imports/exports/base_config.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +module BulkImports + module Exports + class BaseConfig + include Gitlab::Utils::StrongMemoize + + def initialize(exportable) + @exportable = exportable + end + + def exportable_tree + attributes_finder.find_root(exportable_class_sym) + end + + def validate_user_permissions!(user) + user.can?(ability, exportable) || + raise(::Gitlab::ImportExport::Error.permission_error(user, exportable)) + end + + def export_path + strong_memoize(:export_path) do + relative_path = File.join(base_export_path, SecureRandom.hex) + + ::Gitlab::ImportExport.export_path(relative_path: relative_path) + end + end + + def exportable_relations + import_export_config.dig(:tree, exportable_class_sym).keys.map(&:to_s) + end + + private + + attr_reader :exportable + + def attributes_finder + strong_memoize(:attributes_finder) do + ::Gitlab::ImportExport::AttributesFinder.new(config: import_export_config) + end + end + + def import_export_config + ::Gitlab::ImportExport::Config.new(config: import_export_yaml).to_h + end + + def exportable_class + @exportable_class ||= exportable.class + end + + def exportable_class_sym + @exportable_class_sym ||= exportable_class.to_s.downcase.to_sym + end + + def import_export_yaml + raise NotImplementedError + end + + def ability + raise NotImplementedError + end + + def base_export_path + raise NotImplementedError + end + end + end +end diff --git a/app/models/bulk_imports/exports/group_config.rb b/app/models/bulk_imports/exports/group_config.rb new file mode 100644 index 00000000000..30e91373c30 --- /dev/null +++ b/app/models/bulk_imports/exports/group_config.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module BulkImports + module Exports + class GroupConfig < BaseConfig + private + + def base_export_path + exportable.full_path + end + + def import_export_yaml + ::Gitlab::ImportExport.group_config_file + end + + def ability + :admin_group + end + end + end +end diff --git a/app/models/bulk_imports/exports/project_config.rb b/app/models/bulk_imports/exports/project_config.rb new file mode 100644 index 00000000000..be7fe483977 --- /dev/null +++ b/app/models/bulk_imports/exports/project_config.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module BulkImports + module Exports + class ProjectConfig < BaseConfig + private + + def base_export_path + exportable.disk_path + end + + def import_export_yaml + ::Gitlab::ImportExport.config_file + end + + def ability + :admin_project + end + end + end +end diff --git a/app/models/group.rb b/app/models/group.rb index c520a63c6ca..bd9e769dcfe 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -67,6 +67,8 @@ class Group < Namespace has_one :import_state, class_name: 'GroupImportState', inverse_of: :group + has_many :bulk_import_exports, class_name: 'BulkImports::Export', inverse_of: :group + has_many :group_deploy_keys_groups, inverse_of: :group has_many :group_deploy_keys, through: :group_deploy_keys_groups has_many :group_deploy_tokens diff --git a/app/services/clusters/management/create_project_service.rb b/app/services/clusters/management/create_project_service.rb deleted file mode 100644 index 5a0176edd12..00000000000 --- a/app/services/clusters/management/create_project_service.rb +++ /dev/null @@ -1,84 +0,0 @@ -# frozen_string_literal: true - -module Clusters - module Management - class CreateProjectService - CreateError = Class.new(StandardError) - - attr_reader :cluster, :current_user - - def initialize(cluster, current_user:) - @cluster = cluster - @current_user = current_user - end - - def execute - return unless management_project_required? - - project = create_management_project! - update_cluster!(project) - end - - private - - def management_project_required? - Feature.enabled?(:auto_create_cluster_management_project) && cluster.management_project.nil? - end - - def project_params - { - name: project_name, - description: project_description, - namespace_id: namespace.id, - visibility_level: Gitlab::VisibilityLevel::PRIVATE - } - end - - def project_name - "#{cluster.name} Cluster Management" - end - - def project_description - "This project is automatically generated and will be used to manage your Kubernetes cluster. [More information](#{docs_path})" - end - - def docs_path - Rails.application.routes.url_helpers.help_page_path('user/clusters/management_project') - end - - def create_management_project! - ::Projects::CreateService.new(current_user, project_params).execute.tap do |project| - errors = project.errors.full_messages - - if errors.any? - raise CreateError.new("Failed to create project: #{errors}") - end - end - end - - def update_cluster!(project) - unless cluster.update(management_project: project) - raise CreateError.new("Failed to update cluster: #{cluster.errors.full_messages}") - end - end - - def namespace - case cluster.cluster_type - when 'project_type' - cluster.project.namespace - when 'group_type' - cluster.group - when 'instance_type' - instance_administrators_group - else - raise NotImplementedError - end - end - - def instance_administrators_group - Gitlab::CurrentSettings.instance_administrators_group || - raise(CreateError.new('Instance administrators group not found')) - end - end - end -end diff --git a/app/services/groups/count_service.rb b/app/services/groups/count_service.rb index 2a15ae3bc57..735acddb025 100644 --- a/app/services/groups/count_service.rb +++ b/app/services/groups/count_service.rb @@ -19,13 +19,26 @@ module Groups cached_count = Rails.cache.read(cache_key) return cached_count unless cached_count.blank? - refreshed_count = uncached_count - update_cache_for_key(cache_key) { refreshed_count } if refreshed_count > CACHED_COUNT_THRESHOLD - refreshed_count + refresh_cache_over_threshold end - def cache_key - ['groups', "#{issuable_key}_count_service", VERSION, group.id, cache_key_name] + def refresh_cache_over_threshold(key = nil) + key ||= cache_key + new_count = uncached_count + + if new_count > CACHED_COUNT_THRESHOLD + update_cache_for_key(key) { new_count } + else + delete_cache + end + + new_count + end + + def cache_key(key_name = nil) + key_name ||= cache_key_name + + ['groups', "#{issuable_key}_count_service", VERSION, group.id, key_name] end private diff --git a/app/services/groups/open_issues_count_service.rb b/app/services/groups/open_issues_count_service.rb index ef787a04315..17cf3d38987 100644 --- a/app/services/groups/open_issues_count_service.rb +++ b/app/services/groups/open_issues_count_service.rb @@ -6,6 +6,12 @@ module Groups PUBLIC_COUNT_KEY = 'group_public_open_issues_count' TOTAL_COUNT_KEY = 'group_total_open_issues_count' + def clear_all_cache_keys + [cache_key(PUBLIC_COUNT_KEY), cache_key(TOTAL_COUNT_KEY)].each do |key| + Rails.cache.delete(key) + end + end + private def cache_key_name @@ -23,7 +29,14 @@ module Groups end def relation_for_count - IssuesFinder.new(user, group_id: group.id, state: 'opened', non_archived: true, include_subgroups: true, public_only: public_only?).execute + IssuesFinder.new( + user, + group_id: group.id, + state: 'opened', + non_archived: true, + include_subgroups: true, + public_only: public_only? + ).execute end def issuable_key diff --git a/app/services/issuable/bulk_update_service.rb b/app/services/issuable/bulk_update_service.rb index 8bcbb92cd0e..5d5a6db92bb 100644 --- a/app/services/issuable/bulk_update_service.rb +++ b/app/services/issuable/bulk_update_service.rb @@ -15,9 +15,13 @@ module Issuable def execute(type) ids = params.delete(:issuable_ids).split(",") set_update_params(type) - items = update_issuables(type, ids) + updated_issuables = update_issuables(type, ids) - response_success(payload: { count: items.size }) + if updated_issuables.present? && requires_count_cache_reset?(type) + schedule_group_issues_count_reset(updated_issuables) + end + + response_success(payload: { count: updated_issuables.size }) rescue ArgumentError => e response_error(e.message, 422) end @@ -81,6 +85,17 @@ module Issuable def response_error(message, http_status) ServiceResponse.error(message: message, http_status: http_status) end + + def requires_count_cache_reset?(type) + type.to_sym == :issue && params.include?(:state_event) + end + + def schedule_group_issues_count_reset(updated_issuables) + group_ids = updated_issuables.map(&:project).map(&:namespace_id) + return if group_ids.empty? + + Issuables::ClearGroupsIssueCounterWorker.perform_async(group_ids) + end end end diff --git a/app/services/labels/available_labels_service.rb b/app/services/labels/available_labels_service.rb index 1d022740c44..ff29358df86 100644 --- a/app/services/labels/available_labels_service.rb +++ b/app/services/labels/available_labels_service.rb @@ -14,15 +14,16 @@ module Labels return [] unless labels - labels = labels.split(',') if labels.is_a?(String) + labels = labels.split(',').map(&:strip) if labels.is_a?(String) + existing_labels = LabelsFinder.new(current_user, finder_params(labels)).execute.index_by(&:title) labels.map do |label_name| label = Labels::FindOrCreateService.new( current_user, parent, include_ancestor_groups: true, - title: label_name.strip, - available_labels: available_labels + title: label_name, + existing_labels_by_title: existing_labels ).execute(find_only: find_only) label @@ -45,18 +46,19 @@ module Labels private - def finder_params - params = { include_ancestor_groups: true } + def finder_params(titles = nil) + finder_params = { include_ancestor_groups: true } + finder_params[:title] = titles if titles case parent when Group - params[:group_id] = parent.id - params[:only_group_labels] = true + finder_params[:group_id] = parent.id + finder_params[:only_group_labels] = true when Project - params[:project_id] = parent.id + finder_params[:project_id] = parent.id end - params + finder_params end end end diff --git a/app/services/labels/find_or_create_service.rb b/app/services/labels/find_or_create_service.rb index a47dd42aea0..a6fd9843b2a 100644 --- a/app/services/labels/find_or_create_service.rb +++ b/app/services/labels/find_or_create_service.rb @@ -6,6 +6,7 @@ module Labels @current_user = current_user @parent = parent @available_labels = params.delete(:available_labels) + @existing_labels_by_title = params.delete(:existing_labels_by_title) || {} @params = params.dup.with_indifferent_access end @@ -16,7 +17,7 @@ module Labels private - attr_reader :current_user, :parent, :params, :skip_authorization + attr_reader :current_user, :parent, :params, :skip_authorization, :existing_labels_by_title def available_labels @available_labels ||= LabelsFinder.new( @@ -29,9 +30,8 @@ module Labels # Only creates the label if current_user can do so, if the label does not exist # and the user can not create the label, nil is returned - # rubocop: disable CodeReuse/ActiveRecord def find_or_create_label(find_only: false) - new_label = available_labels.find_by(title: title) + new_label = find_existing_label(title) return new_label if find_only @@ -42,6 +42,11 @@ module Labels new_label end + + # rubocop: disable CodeReuse/ActiveRecord + def find_existing_label(title) + existing_labels_by_title[title] || available_labels.find_by(title: title) + end # rubocop: enable CodeReuse/ActiveRecord def title diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 6f1f3309ad9..8662012afdf 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -384,6 +384,7 @@ class NotificationService def send_service_desk_notification(note) return unless note.noteable_type == 'Issue' + return if note.confidential issue = note.noteable recipients = issue.email_participants_emails diff --git a/app/uploaders/bulk_imports/export_uploader.rb b/app/uploaders/bulk_imports/export_uploader.rb new file mode 100644 index 00000000000..356e5ce028e --- /dev/null +++ b/app/uploaders/bulk_imports/export_uploader.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module BulkImports + class ExportUploader < ImportExportUploader + EXTENSION_WHITELIST = %w[ndjson.gz].freeze + end +end diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml index a9134057777..3df39339901 100644 --- a/app/views/profiles/two_factor_auths/show.html.haml +++ b/app/views/profiles/two_factor_auths/show.html.haml @@ -49,7 +49,7 @@ = @error .form-group = label_tag :pin_code, _('Pin code'), class: "label-bold" - = text_field_tag :pin_code, nil, class: "form-control", required: true, data: { qa_selector: 'pin_code_field' } + = text_field_tag :pin_code, nil, class: "form-control gl-form-input", required: true, data: { qa_selector: 'pin_code_field' } .gl-mt-3 = submit_tag _('Register with two-factor app'), class: 'gl-button btn btn-confirm', data: { qa_selector: 'register_2fa_app_button' } diff --git a/app/views/projects/settings/operations/_error_tracking.html.haml b/app/views/projects/settings/operations/_error_tracking.html.haml index 1e77f37ebb4..4ef9e1bd6fb 100644 --- a/app/views/projects/settings/operations/_error_tracking.html.haml +++ b/app/views/projects/settings/operations/_error_tracking.html.haml @@ -9,8 +9,8 @@ %button.gl-button.btn.btn-default.js-settings-toggle{ type: 'button' } = _('Expand') %p - = _('To link Sentry to GitLab, enter your Sentry URL and Auth Token.') - = link_to _('More information'), help_page_path('operations/error_tracking'), target: '_blank', rel: 'noopener noreferrer' + = _('Link Sentry to GitLab to discover and view the errors your application generates.') + = link_to _('Learn more.'), help_page_path('operations/error_tracking'), target: '_blank', rel: 'noopener noreferrer' .settings-content .js-error-tracking-form{ data: { list_projects_endpoint: project_error_tracking_projects_path(@project, format: :json), operations_settings_endpoint: project_settings_operations_path(@project), diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index f663bab950c..de0cdece134 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -2119,6 +2119,15 @@ :weight: 1 :idempotent: :tags: [] +- :name: issuables_clear_groups_issue_counter + :worker_name: Issuables::ClearGroupsIssueCounterWorker + :feature_category: :issue_tracking + :has_external_dependencies: + :urgency: :low + :resource_boundary: :unknown + :weight: 1 + :idempotent: true + :tags: [] - :name: issue_placement :worker_name: IssuePlacementWorker :feature_category: :issue_tracking diff --git a/app/workers/issuables/clear_groups_issue_counter_worker.rb b/app/workers/issuables/clear_groups_issue_counter_worker.rb new file mode 100644 index 00000000000..a8d6fd2f870 --- /dev/null +++ b/app/workers/issuables/clear_groups_issue_counter_worker.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Issuables + class ClearGroupsIssueCounterWorker + include ApplicationWorker + + idempotent! + urgency :low + feature_category :issue_tracking + + def perform(group_ids = []) + return if group_ids.empty? + + groups_with_ancestors = Gitlab::ObjectHierarchy + .new(Group.by_id(group_ids)) + .base_and_ancestors + + clear_cached_count(groups_with_ancestors) + end + + private + + def clear_cached_count(groups) + groups.each do |group| + Groups::OpenIssuesCountService.new(group).clear_all_cache_keys + end + end + end +end diff --git a/app/workers/web_hook_worker.rb b/app/workers/web_hook_worker.rb index 5230f3bfa1f..dffab61dd0e 100644 --- a/app/workers/web_hook_worker.rb +++ b/app/workers/web_hook_worker.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true -class WebHookWorker # rubocop:disable Scalability/IdempotentWorker +# Worker cannot be idempotent: https://gitlab.com/gitlab-org/gitlab/-/issues/218559 +# rubocop:disable Scalability/IdempotentWorker +class WebHookWorker include ApplicationWorker feature_category :integrations @@ -16,3 +18,4 @@ class WebHookWorker # rubocop:disable Scalability/IdempotentWorker WebHookService.new(hook, data, hook_name).execute end end +# rubocop:enable Scalability/IdempotentWorker diff --git a/changelogs/unreleased/194104-epics-iid-too-many-queries.yml b/changelogs/unreleased/194104-epics-iid-too-many-queries.yml new file mode 100644 index 00000000000..e379c0ec680 --- /dev/null +++ b/changelogs/unreleased/194104-epics-iid-too-many-queries.yml @@ -0,0 +1,5 @@ +--- +title: Optimize AvailableLabelsService for multiple labels search +merge_request: 59032 +author: +type: performance diff --git a/changelogs/unreleased/299178-implement-refresh_cache-for-issues-count-in-group-sidebar.yml b/changelogs/unreleased/299178-implement-refresh_cache-for-issues-count-in-group-sidebar.yml new file mode 100644 index 00000000000..58599561e9e --- /dev/null +++ b/changelogs/unreleased/299178-implement-refresh_cache-for-issues-count-in-group-sidebar.yml @@ -0,0 +1,5 @@ +--- +title: Clear group open issues count cache when bulk updating issues state +merge_request: 56386 +author: +type: added diff --git a/changelogs/unreleased/323709-add-package-file-sha-to-api.yml b/changelogs/unreleased/323709-add-package-file-sha-to-api.yml new file mode 100644 index 00000000000..b1b83184424 --- /dev/null +++ b/changelogs/unreleased/323709-add-package-file-sha-to-api.yml @@ -0,0 +1,5 @@ +--- +title: Add sha256 to package file API payload +merge_request: 60631 +author: +type: changed diff --git a/changelogs/unreleased/325944-abstract-participants-dropdown-to-a-shared-component.yml b/changelogs/unreleased/325944-abstract-participants-dropdown-to-a-shared-component.yml new file mode 100644 index 00000000000..4552796d6ff --- /dev/null +++ b/changelogs/unreleased/325944-abstract-participants-dropdown-to-a-shared-component.yml @@ -0,0 +1,5 @@ +--- +title: Resolve Abstract participants dropdown to a shared component +merge_request: 59358 +author: +type: changed diff --git a/changelogs/unreleased/329366-dont-email-issue-email-participants-confidential-comments.yml b/changelogs/unreleased/329366-dont-email-issue-email-participants-confidential-comments.yml new file mode 100644 index 00000000000..d95231bcd48 --- /dev/null +++ b/changelogs/unreleased/329366-dont-email-issue-email-participants-confidential-comments.yml @@ -0,0 +1,5 @@ +--- +title: Don't email issue email participants confidential comments +merge_request: 60594 +author: Lee Tickett @leetickett +type: fixed diff --git a/changelogs/unreleased/error-tracking-settings-clean-up.yml b/changelogs/unreleased/error-tracking-settings-clean-up.yml new file mode 100644 index 00000000000..24c27d12578 --- /dev/null +++ b/changelogs/unreleased/error-tracking-settings-clean-up.yml @@ -0,0 +1,5 @@ +--- +title: Update error tracking settings to use better copy and correct colors +merge_request: 60627 +author: +type: changed diff --git a/changelogs/unreleased/georgekoltsov-add-group-relation-export-models.yml b/changelogs/unreleased/georgekoltsov-add-group-relation-export-models.yml new file mode 100644 index 00000000000..95d96dd3a28 --- /dev/null +++ b/changelogs/unreleased/georgekoltsov-add-group-relation-export-models.yml @@ -0,0 +1,5 @@ +--- +title: Add Group relations export models +merge_request: 59976 +author: +type: added diff --git a/changelogs/unreleased/gl-form-input-2fa-pin.yml b/changelogs/unreleased/gl-form-input-2fa-pin.yml new file mode 100644 index 00000000000..3d1d9e272a2 --- /dev/null +++ b/changelogs/unreleased/gl-form-input-2fa-pin.yml @@ -0,0 +1,5 @@ +--- +title: Add gl-form-input utility class for pin code field in 2fa +merge_request: 59220 +author: Yogi (@yo) +type: changed diff --git a/changelogs/unreleased/kp-add-more-merging-status-phrases.yml b/changelogs/unreleased/kp-add-more-merging-status-phrases.yml new file mode 100644 index 00000000000..0948f94685d --- /dev/null +++ b/changelogs/unreleased/kp-add-more-merging-status-phrases.yml @@ -0,0 +1,5 @@ +--- +title: Show a random predefined message while MR merging is in progress +merge_request: 60521 +author: +type: added diff --git a/changelogs/unreleased/nicolasdular-do-not-set-experiment-cookie-on-self-managed.yml b/changelogs/unreleased/nicolasdular-do-not-set-experiment-cookie-on-self-managed.yml new file mode 100644 index 00000000000..8ed0cf33173 --- /dev/null +++ b/changelogs/unreleased/nicolasdular-do-not-set-experiment-cookie-on-self-managed.yml @@ -0,0 +1,5 @@ +--- +title: Do not set experiment cookie on self managed and delete existing cookies +merge_request: 60419 +author: +type: fixed diff --git a/changelogs/unreleased/remove-cluster-management-project-creation-service.yml b/changelogs/unreleased/remove-cluster-management-project-creation-service.yml new file mode 100644 index 00000000000..4f3aa1a7c9a --- /dev/null +++ b/changelogs/unreleased/remove-cluster-management-project-creation-service.yml @@ -0,0 +1,5 @@ +--- +title: Remove unused :auto_create_cluster_management_project feature flag +merge_request: 60550 +author: +type: other diff --git a/config/feature_flags/development/auto_create_cluster_management_project.yml b/config/feature_flags/development/auto_create_cluster_management_project.yml deleted file mode 100644 index ea7bf349e1e..00000000000 --- a/config/feature_flags/development/auto_create_cluster_management_project.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: auto_create_cluster_management_project -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/23946 -rollout_issue_url: -milestone: '12.10' -type: development -group: group::configure -default_enabled: false diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index 2a3cdad47fc..bec6b6787e5 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -190,6 +190,8 @@ - 1 - - issuable_export_csv - 1 +- - issuables_clear_groups_issue_counter + - 1 - - issue_placement - 2 - - issue_rebalancing diff --git a/db/migrate/20210414100914_add_bulk_import_exports_table.rb b/db/migrate/20210414100914_add_bulk_import_exports_table.rb new file mode 100644 index 00000000000..14a7421c1e4 --- /dev/null +++ b/db/migrate/20210414100914_add_bulk_import_exports_table.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class AddBulkImportExportsTable < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + def up + create_table_with_constraints :bulk_import_exports do |t| + t.bigint :group_id + t.bigint :project_id + t.timestamps_with_timezone null: false + t.integer :status, limit: 2, null: false, default: 0 + t.text :relation, null: false + t.text :jid, unique: true + t.text :error + + t.text_limit :relation, 255 + t.text_limit :jid, 255 + t.text_limit :error, 255 + end + end + + def down + drop_table :bulk_import_exports + end +end diff --git a/db/migrate/20210414130017_add_foreign_key_to_bulk_import_exports_on_project.rb b/db/migrate/20210414130017_add_foreign_key_to_bulk_import_exports_on_project.rb new file mode 100644 index 00000000000..2f7d3713302 --- /dev/null +++ b/db/migrate/20210414130017_add_foreign_key_to_bulk_import_exports_on_project.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AddForeignKeyToBulkImportExportsOnProject < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + def up + add_concurrent_foreign_key :bulk_import_exports, :projects, column: :project_id, on_delete: :cascade + end + + def down + with_lock_retries do + remove_foreign_key :bulk_import_exports, column: :project_id + end + end +end diff --git a/db/migrate/20210414130526_add_foreign_key_to_bulk_import_exports_on_group.rb b/db/migrate/20210414130526_add_foreign_key_to_bulk_import_exports_on_group.rb new file mode 100644 index 00000000000..b7172c6987e --- /dev/null +++ b/db/migrate/20210414130526_add_foreign_key_to_bulk_import_exports_on_group.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AddForeignKeyToBulkImportExportsOnGroup < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + def up + add_concurrent_foreign_key :bulk_import_exports, :namespaces, column: :group_id, on_delete: :cascade + end + + def down + with_lock_retries do + remove_foreign_key :bulk_import_exports, column: :group_id + end + end +end diff --git a/db/migrate/20210414131807_add_bulk_import_exports_table_indexes.rb b/db/migrate/20210414131807_add_bulk_import_exports_table_indexes.rb new file mode 100644 index 00000000000..1cbd1cadf5e --- /dev/null +++ b/db/migrate/20210414131807_add_bulk_import_exports_table_indexes.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +class AddBulkImportExportsTableIndexes < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + GROUP_INDEX_NAME = 'partial_index_bulk_import_exports_on_group_id_and_relation' + PROJECT_INDEX_NAME = 'partial_index_bulk_import_exports_on_project_id_and_relation' + + def up + add_concurrent_index :bulk_import_exports, + [:group_id, :relation], + unique: true, + where: 'group_id IS NOT NULL', + name: GROUP_INDEX_NAME + + add_concurrent_index :bulk_import_exports, + [:project_id, :relation], + unique: true, + where: 'project_id IS NOT NULL', + name: PROJECT_INDEX_NAME + end + + def down + remove_concurrent_index_by_name(:bulk_import_exports, GROUP_INDEX_NAME) + remove_concurrent_index_by_name(:bulk_import_exports, PROJECT_INDEX_NAME) + end +end diff --git a/db/migrate/20210414133310_add_bulk_import_export_uploads_table.rb b/db/migrate/20210414133310_add_bulk_import_export_uploads_table.rb new file mode 100644 index 00000000000..d20e57848e9 --- /dev/null +++ b/db/migrate/20210414133310_add_bulk_import_export_uploads_table.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AddBulkImportExportUploadsTable < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + def up + create_table_with_constraints :bulk_import_export_uploads do |t| + t.references :export, index: true, null: false, foreign_key: { to_table: :bulk_import_exports, on_delete: :cascade } + t.datetime_with_timezone :updated_at, null: false + t.text :export_file + + t.text_limit :export_file, 255 + end + end + + def down + drop_table :bulk_import_export_uploads + end +end diff --git a/db/schema_migrations/20210414100914 b/db/schema_migrations/20210414100914 new file mode 100644 index 00000000000..dcbc93d9987 --- /dev/null +++ b/db/schema_migrations/20210414100914 @@ -0,0 +1 @@ +4950567ba7071183bc008936e4bbe1391dd0100c5caa2a6821be85dc3d2423fc
\ No newline at end of file diff --git a/db/schema_migrations/20210414130017 b/db/schema_migrations/20210414130017 new file mode 100644 index 00000000000..0eaffe4ddd1 --- /dev/null +++ b/db/schema_migrations/20210414130017 @@ -0,0 +1 @@ +202409998a03fd29c52e3ee9546ab8ec7aa3c56173ee755e9342f1cc6a5f1f6b
\ No newline at end of file diff --git a/db/schema_migrations/20210414130526 b/db/schema_migrations/20210414130526 new file mode 100644 index 00000000000..ebba5c47f22 --- /dev/null +++ b/db/schema_migrations/20210414130526 @@ -0,0 +1 @@ +2343decc3abb79b38bcde6aba5a8fd208842096d7fb7a4c51872f66f1a125296
\ No newline at end of file diff --git a/db/schema_migrations/20210414131807 b/db/schema_migrations/20210414131807 new file mode 100644 index 00000000000..9a7800b86f8 --- /dev/null +++ b/db/schema_migrations/20210414131807 @@ -0,0 +1 @@ +4db08c0fecd210b329492596cf029518484d256bdb06efff233b3a38677fd6a6
\ No newline at end of file diff --git a/db/schema_migrations/20210414133310 b/db/schema_migrations/20210414133310 new file mode 100644 index 00000000000..9a0a224e09b --- /dev/null +++ b/db/schema_migrations/20210414133310 @@ -0,0 +1 @@ +f306cf9553e4bd237cfdff31d5432d4ff44302a923e475c477f76d32ccb4d257
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index f52f5457dd2..3cb671164c1 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -10212,6 +10212,47 @@ CREATE SEQUENCE bulk_import_entities_id_seq ALTER SEQUENCE bulk_import_entities_id_seq OWNED BY bulk_import_entities.id; +CREATE TABLE bulk_import_export_uploads ( + id bigint NOT NULL, + export_id bigint NOT NULL, + updated_at timestamp with time zone NOT NULL, + export_file text, + CONSTRAINT check_5add76239d CHECK ((char_length(export_file) <= 255)) +); + +CREATE SEQUENCE bulk_import_export_uploads_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE bulk_import_export_uploads_id_seq OWNED BY bulk_import_export_uploads.id; + +CREATE TABLE bulk_import_exports ( + id bigint NOT NULL, + group_id bigint, + project_id bigint, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + status smallint DEFAULT 0 NOT NULL, + relation text NOT NULL, + jid text, + error text, + CONSTRAINT check_24cb010672 CHECK ((char_length(relation) <= 255)), + CONSTRAINT check_8f0f357334 CHECK ((char_length(error) <= 255)), + CONSTRAINT check_9ee6d14d33 CHECK ((char_length(jid) <= 255)) +); + +CREATE SEQUENCE bulk_import_exports_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE bulk_import_exports_id_seq OWNED BY bulk_import_exports.id; + CREATE TABLE bulk_import_failures ( id bigint NOT NULL, bulk_import_entity_id bigint NOT NULL, @@ -19268,6 +19309,10 @@ ALTER TABLE ONLY bulk_import_configurations ALTER COLUMN id SET DEFAULT nextval( ALTER TABLE ONLY bulk_import_entities ALTER COLUMN id SET DEFAULT nextval('bulk_import_entities_id_seq'::regclass); +ALTER TABLE ONLY bulk_import_export_uploads ALTER COLUMN id SET DEFAULT nextval('bulk_import_export_uploads_id_seq'::regclass); + +ALTER TABLE ONLY bulk_import_exports ALTER COLUMN id SET DEFAULT nextval('bulk_import_exports_id_seq'::regclass); + ALTER TABLE ONLY bulk_import_failures ALTER COLUMN id SET DEFAULT nextval('bulk_import_failures_id_seq'::regclass); ALTER TABLE ONLY bulk_import_trackers ALTER COLUMN id SET DEFAULT nextval('bulk_import_trackers_id_seq'::regclass); @@ -20387,6 +20432,12 @@ ALTER TABLE ONLY bulk_import_configurations ALTER TABLE ONLY bulk_import_entities ADD CONSTRAINT bulk_import_entities_pkey PRIMARY KEY (id); +ALTER TABLE ONLY bulk_import_export_uploads + ADD CONSTRAINT bulk_import_export_uploads_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY bulk_import_exports + ADD CONSTRAINT bulk_import_exports_pkey PRIMARY KEY (id); + ALTER TABLE ONLY bulk_import_failures ADD CONSTRAINT bulk_import_failures_pkey PRIMARY KEY (id); @@ -22207,6 +22258,8 @@ CREATE INDEX index_bulk_import_entities_on_parent_id ON bulk_import_entities USI CREATE INDEX index_bulk_import_entities_on_project_id ON bulk_import_entities USING btree (project_id); +CREATE INDEX index_bulk_import_export_uploads_on_export_id ON bulk_import_export_uploads USING btree (export_id); + CREATE INDEX index_bulk_import_failures_on_bulk_import_entity_id ON bulk_import_failures USING btree (bulk_import_entity_id); CREATE INDEX index_bulk_import_failures_on_correlation_id_value ON bulk_import_failures USING btree (correlation_id_value); @@ -24511,6 +24564,10 @@ CREATE INDEX packages_packages_needs_verification ON packages_package_files USIN CREATE INDEX packages_packages_pending_verification ON packages_package_files USING btree (verified_at NULLS FIRST) WHERE (verification_state = 0); +CREATE UNIQUE INDEX partial_index_bulk_import_exports_on_group_id_and_relation ON bulk_import_exports USING btree (group_id, relation) WHERE (group_id IS NOT NULL); + +CREATE UNIQUE INDEX partial_index_bulk_import_exports_on_project_id_and_relation ON bulk_import_exports USING btree (project_id, relation) WHERE (project_id IS NOT NULL); + CREATE INDEX partial_index_ci_builds_on_scheduled_at_with_scheduled_jobs ON ci_builds USING btree (scheduled_at) WHERE ((scheduled_at IS NOT NULL) AND ((type)::text = 'Ci::Build'::text) AND ((status)::text = 'scheduled'::text)); CREATE INDEX partial_index_deployments_for_legacy_successful_deployments ON deployments USING btree (id) WHERE ((finished_at IS NULL) AND (status = 2)); @@ -25002,6 +25059,9 @@ ALTER TABLE ONLY sprints ALTER TABLE ONLY push_event_payloads ADD CONSTRAINT fk_36c74129da FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE; +ALTER TABLE ONLY bulk_import_exports + ADD CONSTRAINT fk_39c726d3b5 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; + ALTER TABLE ONLY ci_builds ADD CONSTRAINT fk_3a9eaa254d FOREIGN KEY (stage_id) REFERENCES ci_stages(id) ON DELETE CASCADE; @@ -25197,6 +25257,9 @@ ALTER TABLE ONLY issues ALTER TABLE ONLY protected_branch_merge_access_levels ADD CONSTRAINT fk_8a3072ccb3 FOREIGN KEY (protected_branch_id) REFERENCES protected_branches(id) ON DELETE CASCADE; +ALTER TABLE ONLY bulk_import_exports + ADD CONSTRAINT fk_8c6f33cebe FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE; + ALTER TABLE ONLY releases ADD CONSTRAINT fk_8e4456f90f FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE SET NULL; @@ -26859,6 +26922,9 @@ ALTER TABLE ONLY incident_management_oncall_shifts ALTER TABLE ONLY analytics_cycle_analytics_group_stages ADD CONSTRAINT fk_rails_dfb37c880d FOREIGN KEY (end_event_label_id) REFERENCES labels(id) ON DELETE CASCADE; +ALTER TABLE ONLY bulk_import_export_uploads + ADD CONSTRAINT fk_rails_dfbfb45eca FOREIGN KEY (export_id) REFERENCES bulk_import_exports(id) ON DELETE CASCADE; + ALTER TABLE ONLY label_priorities ADD CONSTRAINT fk_rails_e161058b0f FOREIGN KEY (label_id) REFERENCES labels(id) ON DELETE CASCADE; diff --git a/doc/api/packages.md b/doc/api/packages.md index 112c7ef2e61..7ad99475515 100644 --- a/doc/api/packages.md +++ b/doc/api/packages.md @@ -287,6 +287,7 @@ Example response: "size": 2421, "file_md5": "58e6a45a629910c6ff99145a688971ac", "file_sha1": "ebd193463d3915d7e22219f52740056dfd26cbfe", + "file_sha256": "a903393463d3915d7e22219f52740056dfd26cbfeff321b", "pipelines": [ { "id": 123, @@ -310,7 +311,8 @@ Example response: "file_name": "my-app-1.5-20181107.152550-1.pom", "size": 1122, "file_md5": "d90f11d851e17c5513586b4a7e98f1b2", - "file_sha1": "9608d068fe88aff85781811a42f32d97feb440b5" + "file_sha1": "9608d068fe88aff85781811a42f32d97feb440b5", + "file_sha256": "2987d068fe88aff85781811a42f32d97feb4f092a399" }, { "id": 27, @@ -319,7 +321,8 @@ Example response: "file_name": "maven-metadata.xml", "size": 767, "file_md5": "6dfd0cce1203145a927fef5e3a1c650c", - "file_sha1": "d25932de56052d320a8ac156f745ece73f6a8cd2" + "file_sha1": "d25932de56052d320a8ac156f745ece73f6a8cd2", + "file_sha256": "ac849d002e56052d320a8ac156f745ece73f6a8cd2f3e82" } ] ``` diff --git a/lib/api/entities/package_file.rb b/lib/api/entities/package_file.rb index 2cc2f62a948..e34a6a7aa1d 100644 --- a/lib/api/entities/package_file.rb +++ b/lib/api/entities/package_file.rb @@ -5,7 +5,7 @@ module API class PackageFile < Grape::Entity expose :id, :package_id, :created_at expose :file_name, :size - expose :file_md5, :file_sha1 + expose :file_md5, :file_sha1, :file_sha256 expose :pipelines, if: ->(package_file) { package_file.pipelines.present? }, using: Package::Pipeline end end diff --git a/lib/gitlab/experimentation/controller_concern.rb b/lib/gitlab/experimentation/controller_concern.rb index 248abfeada5..e53689eb89b 100644 --- a/lib/gitlab/experimentation/controller_concern.rb +++ b/lib/gitlab/experimentation/controller_concern.rb @@ -19,13 +19,18 @@ module Gitlab end def set_experimentation_subject_id_cookie - return if cookies[:experimentation_subject_id].present? - - cookies.permanent.signed[:experimentation_subject_id] = { - value: SecureRandom.uuid, - secure: ::Gitlab.config.gitlab.https, - httponly: true - } + if Gitlab.dev_env_or_com? + return if cookies[:experimentation_subject_id].present? + + cookies.permanent.signed[:experimentation_subject_id] = { + value: SecureRandom.uuid, + secure: ::Gitlab.config.gitlab.https, + httponly: true + } + else + # We set the cookie before, although experiments are not conducted on self managed instances. + cookies.delete(:experimentation_subject_id) + end end def push_frontend_experiment(experiment_key, subject: nil) diff --git a/lib/gitlab/import_export/error.rb b/lib/gitlab/import_export/error.rb index f11b7a0a298..4af6b03fe94 100644 --- a/lib/gitlab/import_export/error.rb +++ b/lib/gitlab/import_export/error.rb @@ -3,12 +3,20 @@ module Gitlab module ImportExport class Error < StandardError - def self.permission_error(user, importable) + def self.permission_error(user, object) self.new( "User with ID: %s does not have required permissions for %s: %s with ID: %s" % - [user.id, importable.class.name, importable.name, importable.id] + [user.id, object.class.name, object.name, object.id] ) end + + def self.unsupported_object_type_error + self.new('Unknown object type') + end + + def self.file_compression_error + self.new('File compression failed') + end end end end diff --git a/lib/gitlab/middleware/speedscope.rb b/lib/gitlab/middleware/speedscope.rb index 547aab9c797..74f334d9ab3 100644 --- a/lib/gitlab/middleware/speedscope.rb +++ b/lib/gitlab/middleware/speedscope.rb @@ -44,7 +44,7 @@ module Gitlab headers = { 'Content-Type' => 'text/html' } path = request.env['PATH_INFO'].sub('//', '/') - speedscope_url = ActionController::Base.helpers.asset_url('/-/speedscope/index.html') + speedscope_path = ::Gitlab::Utils.append_path(::Gitlab.config.gitlab.relative_url_root, '/-/speedscope/index.html') html = <<~HTML <!DOCTYPE html> @@ -64,7 +64,7 @@ module Gitlab var iframe = document.createElement('IFRAME'); iframe.setAttribute('id', 'speedscope-iframe'); document.body.appendChild(iframe); - var iframeUrl = '#{speedscope_url}#profileURL=' + objUrl + '&title=' + 'Flamegraph for #{CGI.escape(path)}'; + var iframeUrl = '#{speedscope_path}#profileURL=' + objUrl + '&title=' + 'Flamegraph for #{CGI.escape(path)}'; iframe.setAttribute('src', iframeUrl); </script> </body> diff --git a/lib/learn_gitlab.rb b/lib/learn_gitlab.rb deleted file mode 100644 index abceb80bd30..00000000000 --- a/lib/learn_gitlab.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -class LearnGitlab - PROJECT_NAME = 'Learn GitLab' - BOARD_NAME = 'GitLab onboarding' - LABEL_NAME = 'Novice' - - def initialize(current_user) - @current_user = current_user - end - - def available? - project && board && label - end - - def project - @project ||= current_user.projects.find_by_name(PROJECT_NAME) - end - - def board - return unless project - - @board ||= project.boards.find_by_name(BOARD_NAME) - end - - def label - return unless project - - @label ||= project.labels.find_by_name(LABEL_NAME) - end - - private - - attr_reader :current_user -end diff --git a/lib/learn_gitlab/onboarding.rb b/lib/learn_gitlab/onboarding.rb new file mode 100644 index 00000000000..38ffa9eb2e6 --- /dev/null +++ b/lib/learn_gitlab/onboarding.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module LearnGitlab + class Onboarding + include Gitlab::Utils::StrongMemoize + + ACTION_ISSUE_IDS = { + issue_created: 4, + git_write: 6, + pipeline_created: 7, + merge_request_created: 9, + user_added: 8, + trial_started: 2, + required_mr_approvals_enabled: 11, + code_owners_enabled: 10 + }.freeze + + ACTION_DOC_URLS = { + security_scan_enabled: 'https://docs.gitlab.com/ee/user/application_security/security_dashboard/#gitlab-security-dashboard-security-center-and-vulnerability-reports' + }.freeze + + def initialize(namespace) + @namespace = namespace + end + + def completed_percentage + return 0 unless onboarding_progress + + attributes = onboarding_progress.attributes.symbolize_keys + + total_actions = action_columns.count + completed_actions = action_columns.count { |column| attributes[column].present? } + + (completed_actions.to_f / total_actions.to_f * 100).round + end + + private + + def onboarding_progress + strong_memoize(:onboarding_progress) do + OnboardingProgress.find_by(namespace: namespace) # rubocop: disable CodeReuse/ActiveRecord + end + end + + def action_columns + strong_memoize(:action_columns) do + tracked_actions.map { |action_key| OnboardingProgress.column_name(action_key) } + end + end + + def tracked_actions + ACTION_ISSUE_IDS.keys + ACTION_DOC_URLS.keys + end + + attr_reader :namespace + end +end diff --git a/lib/learn_gitlab/project.rb b/lib/learn_gitlab/project.rb new file mode 100644 index 00000000000..599f9940e53 --- /dev/null +++ b/lib/learn_gitlab/project.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module LearnGitlab + class Project + PROJECT_NAME = 'Learn GitLab' + BOARD_NAME = 'GitLab onboarding' + LABEL_NAME = 'Novice' + + def initialize(current_user) + @current_user = current_user + end + + def available? + project && board && label + end + + def project + @project ||= current_user.projects.find_by_name(PROJECT_NAME) + end + + def board + return unless project + + @board ||= project.boards.find_by_name(BOARD_NAME) + end + + def label + return unless project + + @label ||= project.labels.find_by_name(LABEL_NAME) + end + + private + + attr_reader :current_user + end +end diff --git a/lib/sidebars/projects/menus/learn_gitlab_menu.rb b/lib/sidebars/projects/menus/learn_gitlab_menu.rb index 7fb59278a9b..62596c4fbd7 100644 --- a/lib/sidebars/projects/menus/learn_gitlab_menu.rb +++ b/lib/sidebars/projects/menus/learn_gitlab_menu.rb @@ -4,6 +4,8 @@ module Sidebars module Projects module Menus class LearnGitlabMenu < ::Sidebars::Menu + include Gitlab::Utils::StrongMemoize + override :link def link project_learn_gitlab_path(context.project) @@ -19,6 +21,20 @@ module Sidebars _('Learn GitLab') end + override :has_pill? + def has_pill? + context.learn_gitlab_experiment_enabled + end + + override :pill_count + def pill_count + strong_memoize(:pill_count) do + percentage = LearnGitlab::Onboarding.new(context.project.namespace).completed_percentage + + "#{percentage}%" + end + end + override :extra_container_html_options def nav_link_html_options { class: 'home' } diff --git a/locale/gitlab.pot b/locale/gitlab.pot index ade7794779d..e91678678a0 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -3673,9 +3673,6 @@ msgstr "" msgid "An error occurred while saving changes: %{error}" msgstr "" -msgid "An error occurred while searching users." -msgstr "" - msgid "An error occurred while subscribing to notifications." msgstr "" @@ -12932,6 +12929,9 @@ msgstr "" msgid "ErrorTracking|Connection failed. Check Auth Token and try again." msgstr "" +msgid "ErrorTracking|Enable error tracking" +msgstr "" + msgid "ErrorTracking|If you self-host Sentry, enter your Sentry instance's full URL. If you use Sentry's hosted solution, enter https://sentry.io" msgstr "" @@ -19396,6 +19396,9 @@ msgstr "" msgid "Link Prometheus monitoring to GitLab." msgstr "" +msgid "Link Sentry to GitLab to discover and view the errors your application generates." +msgstr "" + msgid "Link an external wiki from the project's sidebar. %{docs_link}" msgstr "" @@ -26494,9 +26497,6 @@ msgstr "" msgid "Recent" msgstr "" -msgid "Recent Activity" -msgstr "" - msgid "Recent Deliveries" msgstr "" @@ -33453,9 +33453,6 @@ msgstr "" msgid "To learn more about this project, read %{link_to_wiki}." msgstr "" -msgid "To link Sentry to GitLab, enter your Sentry URL and Auth Token." -msgstr "" - msgid "To move or copy an entire GitLab project from another GitLab installation to this one, navigate to the original project's settings page, generate an export file, and upload it here." msgstr "" @@ -35150,12 +35147,21 @@ msgstr "" msgid "ValueStreamAnalytics|<1m" msgstr "" +msgid "ValueStreamAnalytics|Average number of deployments to production per day." +msgstr "" + msgid "ValueStreamAnalytics|Median time from first commit to issue closed." msgstr "" msgid "ValueStreamAnalytics|Median time from issue created to issue closed." msgstr "" +msgid "ValueStreamAnalytics|Number of new issues created." +msgstr "" + +msgid "ValueStreamAnalytics|Total number of deploys to production." +msgstr "" + msgid "ValueStreamEvent|Stage time (median)" msgstr "" @@ -38222,9 +38228,24 @@ msgstr "" msgid "mrWidget|Merged by" msgstr "" +msgid "mrWidget|Merging! Changes are being shipped…" +msgstr "" + +msgid "mrWidget|Merging! Changes will land soon…" +msgstr "" + msgid "mrWidget|Merging! Drum roll, please…" msgstr "" +msgid "mrWidget|Merging! Everything's good…" +msgstr "" + +msgid "mrWidget|Merging! This is going to be great…" +msgstr "" + +msgid "mrWidget|Merging! We're almost there…" +msgstr "" + msgid "mrWidget|More information" msgstr "" diff --git a/package.json b/package.json index 58d05c9562b..e35243dacdc 100644 --- a/package.json +++ b/package.json @@ -233,11 +233,11 @@ "md5": "^2.2.1", "miragejs": "^0.1.40", "mock-apollo-client": "^0.5.0", - "node-sass": "^4.14.1", "nodemon": "^2.0.4", "postcss": "^7.0.14", "prettier": "2.2.1", "readdir-enhanced": "^2.2.4", + "sass": "^1.32.12", "timezone-mock": "^1.0.8", "vue-jest": "4.0.1", "webpack-dev-server": "^3.11.2", diff --git a/scripts/frontend/stylelint/stylelint-utility-map.js b/scripts/frontend/stylelint/stylelint-utility-map.js index 545aade9ccc..676f83cd067 100644 --- a/scripts/frontend/stylelint/stylelint-utility-map.js +++ b/scripts/frontend/stylelint/stylelint-utility-map.js @@ -1,8 +1,8 @@ const fs = require('fs'); -const sass = require('node-sass'); const path = require('path'); const postcss = require('postcss'); const prettier = require('prettier'); +const sass = require('sass'); const utils = require('./stylelint-utils'); diff --git a/scripts/frontend/stylelint/utility-classes-map.js b/scripts/frontend/stylelint/utility-classes-map.js index 1929f950f6c..a174812ff93 100644 --- a/scripts/frontend/stylelint/utility-classes-map.js +++ b/scripts/frontend/stylelint/utility-classes-map.js @@ -1,216 +1,259 @@ module.exports = { '99097f29a9473b56eacdb9ff0681c366': { selectors: ['.align-baseline (1:1)'] }, - d969b318bb994e104e8c965006d71cb7: { selectors: ['.align-top (4:1)'] }, - '8cd54ab97b9cc43cb9d13d2ea7c601c7': { selectors: ['.align-middle (7:1)'] }, - dd06eb6c49e979b7a9fdaa7119aa0a0b: { selectors: ['.align-bottom (10:1)'] }, - '0af1e90cbc468615e299ec9f49e97c4a': { selectors: ['.align-text-bottom (13:1)'] }, - '50af706df238cf59bdc634fc684ba0c9': { selectors: ['.align-text-top (16:1)'] }, - c968922e6e47445362129a684b5913c0: { selectors: ['.bg-primary (19:1)'] }, - '3c397f9786c24cff4779a11cf5b3d7e7': { selectors: ['.bg-secondary (27:1)'] }, - '659677469a4477267fabc1788f7cad4e': { selectors: ['.bg-success (35:1)'] }, - '56d246d5b6a708a4c6f78dbd2444106c': { selectors: ['.bg-info (43:1)'] }, - '6bec0a33df3a6380c30103db5c273455': { selectors: ['.bg-warning (51:1)'] }, - '0ce5d074c8667ce6c32360658f428d5d': { selectors: ['.bg-danger (59:1)'] }, - '0d0269c62a01e97caa9039d227a25d12': { selectors: ['.bg-light (67:1)'] }, - '3a56309ad8c5b46ebcc3b13fe1987ac1': { selectors: ['.bg-dark (75:1)'] }, - '0e252f8dd392a33343d3d5efc1e3194a': { selectors: ['.bg-white (83:1)'] }, - '3af6f52f0ed4f98e797d5f10a35ca6bc': { selectors: ['.bg-transparent (86:1)'] }, - '16da7fdce74577ceab356509db565612': { selectors: ['.border (89:1)'] }, - '929622517ca05efde3b51e5f1a57064e': { selectors: ['.border-top (92:1)'] }, - '7283090353df54f1d515a6ceddfb9693': { selectors: ['.border-right (95:1)'] }, - bd5670d71332c652b46db82949042e31: { selectors: ['.border-bottom (98:1)'] }, - fa71e003d04734a898a85cc5285e3cbb: { selectors: ['.border-left (101:1)'] }, - ed482cea071e316f29d78fd93c3f3644: { selectors: ['.border-0 (104:1)'] }, - '90cb661baf21e10be6e479cb0544b1a7': { selectors: ['.border-top-0 (107:1)'] }, - '8a32707eaa09fc998bf8cc915710b60c': { selectors: ['.border-right-0 (110:1)'] }, - a6f01957e142a000e7742b31ac6c2331: { selectors: ['.border-bottom-0 (113:1)'] }, - c740fe952cc1985ee14f7d1c7a359a29: { selectors: ['.border-left-0 (116:1)'] }, - af9dd93e9780306ffa4bb25a6384902f: { selectors: ['.border-primary (119:1)'] }, - afa290dfe58cca06be5924ceae1b019b: { selectors: ['.border-secondary (122:1)'] }, - '9b1ac460bdddf1e0164d7bf988cc2da8': { selectors: ['.border-success (125:1)'] }, - '091cbf41d6be12061382fa571ee1ce82': { selectors: ['.border-info (128:1)'] }, - '3ada321d4a387901dad6d80e1b6be3fd': { selectors: ['.border-warning (131:1)'] }, - '13b4713dd52c1e359d1b43dd658cb249': { selectors: ['.border-danger (134:1)'] }, - '0048e110875ea22b04104d55e764a367': { selectors: ['.border-light (137:1)'] }, - a900b6b567c9a911326cdd0e19f40f8e: { selectors: ['.border-dark (140:1)'] }, - '78bcd867ac9677c743c2bc33b872f27b': { selectors: ['.border-white (143:1)'] }, - e0fc10c49c7b7f4d1924336d21a4f64e: { selectors: ['.rounded (146:1)'] }, - '1b74b9d0a7d6a59281b5b5cae43c859a': { selectors: ['.rounded-top (149:1)'] }, - '20b75f55f39e662e038d51a6442c03df': { selectors: ['.rounded-right (153:1)'] }, - '83ea6db794873239c21f44af25618677': { selectors: ['.rounded-bottom (157:1)'] }, - '8464e9e8001e65dfc06397436a5eebd7': { selectors: ['.rounded-left (161:1)'] }, - '59c2f788287fa43caf5891adfc5c796e': { selectors: ['.rounded-circle (165:1)'] }, - '31a632ba94f8c41558bd6044458f1459': { selectors: ['.rounded-0 (168:1)'] }, - '16aaf53ab29d6b248b0257f2fa413914': { selectors: ['.d-none (176:1)'] }, - '4f42736ac9217039ed791b4306e60aeb': { selectors: ['.d-inline (179:1)'] }, - '067efa04b76649e8afcdceb9f5f7e870': { selectors: ['.d-inline-block (182:1)'] }, - de54f49149fb9b512aa79ad9ada838f2: { selectors: ['.d-block (185:1)'] }, - '80fc32acbc0c28ee890a160c23529d26': { selectors: ['.d-table (188:1)'] }, - '6a87b1db48298ca94cbe5dee79a6eed1': { selectors: ['.d-table-row (191:1)'] }, - b9896f0d94760bf5920f47904e9f7512: { selectors: ['.d-table-cell (194:1)'] }, - d25c51f38c4d057209b96c664de68c44: { selectors: ['.d-flex (197:1)'] }, - e72d46b636d5b8e17e771daa95793f33: { selectors: ['.d-inline-flex (200:1)'] }, - '2c433b7c14a5ae32cfa8ec7867ee8526': { selectors: ['.embed-responsive (303:1)'] }, + d969b318bb994e104e8c965006d71cb7: { selectors: ['.align-top (5:1)'] }, + '8cd54ab97b9cc43cb9d13d2ea7c601c7': { selectors: ['.align-middle (9:1)'] }, + dd06eb6c49e979b7a9fdaa7119aa0a0b: { selectors: ['.align-bottom (13:1)'] }, + '0af1e90cbc468615e299ec9f49e97c4a': { selectors: ['.align-text-bottom (17:1)'] }, + '50af706df238cf59bdc634fc684ba0c9': { selectors: ['.align-text-top (21:1)'] }, + c968922e6e47445362129a684b5913c0: { selectors: ['.bg-primary (25:1)'] }, + '3c397f9786c24cff4779a11cf5b3d7e7': { selectors: ['.bg-secondary (35:1)'] }, + '659677469a4477267fabc1788f7cad4e': { selectors: ['.bg-success (45:1)'] }, + '56d246d5b6a708a4c6f78dbd2444106c': { selectors: ['.bg-info (55:1)'] }, + '6bec0a33df3a6380c30103db5c273455': { selectors: ['.bg-warning (65:1)'] }, + '0ce5d074c8667ce6c32360658f428d5d': { selectors: ['.bg-danger (75:1)'] }, + '0d0269c62a01e97caa9039d227a25d12': { selectors: ['.bg-light (85:1)'] }, + '3a56309ad8c5b46ebcc3b13fe1987ac1': { selectors: ['.bg-dark (95:1)'] }, + '0e252f8dd392a33343d3d5efc1e3194a': { selectors: ['.bg-white (105:1)'] }, + '3af6f52f0ed4f98e797d5f10a35ca6bc': { selectors: ['.bg-transparent (109:1)'] }, + '16da7fdce74577ceab356509db565612': { selectors: ['.border (113:1)'] }, + '929622517ca05efde3b51e5f1a57064e': { selectors: ['.border-top (117:1)'] }, + '7283090353df54f1d515a6ceddfb9693': { selectors: ['.border-right (121:1)'] }, + bd5670d71332c652b46db82949042e31: { selectors: ['.border-bottom (125:1)'] }, + fa71e003d04734a898a85cc5285e3cbb: { selectors: ['.border-left (129:1)'] }, + ed482cea071e316f29d78fd93c3f3644: { selectors: ['.border-0 (133:1)'] }, + '90cb661baf21e10be6e479cb0544b1a7': { selectors: ['.border-top-0 (137:1)'] }, + '8a32707eaa09fc998bf8cc915710b60c': { selectors: ['.border-right-0 (141:1)'] }, + a6f01957e142a000e7742b31ac6c2331: { selectors: ['.border-bottom-0 (145:1)'] }, + c740fe952cc1985ee14f7d1c7a359a29: { selectors: ['.border-left-0 (149:1)'] }, + af9dd93e9780306ffa4bb25a6384902f: { selectors: ['.border-primary (153:1)'] }, + afa290dfe58cca06be5924ceae1b019b: { selectors: ['.border-secondary (157:1)'] }, + '9b1ac460bdddf1e0164d7bf988cc2da8': { selectors: ['.border-success (161:1)'] }, + '091cbf41d6be12061382fa571ee1ce82': { selectors: ['.border-info (165:1)'] }, + '3ada321d4a387901dad6d80e1b6be3fd': { selectors: ['.border-warning (169:1)'] }, + '13b4713dd52c1e359d1b43dd658cb249': { selectors: ['.border-danger (173:1)'] }, + '0048e110875ea22b04104d55e764a367': { selectors: ['.border-light (177:1)'] }, + a900b6b567c9a911326cdd0e19f40f8e: { selectors: ['.border-dark (181:1)'] }, + '78bcd867ac9677c743c2bc33b872f27b': { selectors: ['.border-white (185:1)'] }, + '35ef133b860874a5879e9451c491bc1c': { selectors: ['.rounded-sm (189:1)'] }, + e0fc10c49c7b7f4d1924336d21a4f64e: { selectors: ['.rounded (193:1)'] }, + '1b74b9d0a7d6a59281b5b5cae43c859a': { selectors: ['.rounded-top (197:1)'] }, + '20b75f55f39e662e038d51a6442c03df': { selectors: ['.rounded-right (202:1)'] }, + '83ea6db794873239c21f44af25618677': { selectors: ['.rounded-bottom (207:1)'] }, + '8464e9e8001e65dfc06397436a5eebd7': { selectors: ['.rounded-left (212:1)'] }, + a1148f40e8c509b2bcc829e2181c7582: { selectors: ['.rounded-lg (217:1)'] }, + '59c2f788287fa43caf5891adfc5c796e': { selectors: ['.rounded-circle (221:1)'] }, + '7f35b0a4b74ee7174b13cb841df0bdb3': { selectors: ['.rounded-pill (225:1)'] }, + '31a632ba94f8c41558bd6044458f1459': { selectors: ['.rounded-0 (229:1)'] }, + '16aaf53ab29d6b248b0257f2fa413914': { selectors: ['.d-none (239:1)'] }, + '4f42736ac9217039ed791b4306e60aeb': { selectors: ['.d-inline (243:1)'] }, + '067efa04b76649e8afcdceb9f5f7e870': { selectors: ['.d-inline-block (247:1)'] }, + de54f49149fb9b512aa79ad9ada838f2: { selectors: ['.d-block (251:1)'] }, + '80fc32acbc0c28ee890a160c23529d26': { selectors: ['.d-table (255:1)'] }, + '6a87b1db48298ca94cbe5dee79a6eed1': { selectors: ['.d-table-row (259:1)'] }, + b9896f0d94760bf5920f47904e9f7512: { selectors: ['.d-table-cell (263:1)'] }, + d25c51f38c4d057209b96c664de68c44: { selectors: ['.d-flex (267:1)'] }, + e72d46b636d5b8e17e771daa95793f33: { selectors: ['.d-inline-flex (271:1)'] }, + '2c433b7c14a5ae32cfa8ec7867ee8526': { selectors: ['.embed-responsive (460:1)'] }, '56b318b8d8eb845b769d60cefcd131bb': { selectors: [ - '.embed-responsive .embed-responsive-item, .embed-responsive iframe, .embed-responsive embed, .embed-responsive object, .embed-responsive video (312:3)', + '.embed-responsive .embed-responsive-item, .embed-responsive iframe, .embed-responsive embed, .embed-responsive object, .embed-responsive video (471:1)', ], }, - c5009af89633c4d2f71a0a9fa333630d: { selectors: ['.flex-row (337:1)'] }, - '7b06a3d956579cd64b6f5b1a57255369': { selectors: ['.flex-column (340:1)'] }, - '21744a8c4dc6ed1519903b4236b00af4': { selectors: ['.flex-row-reverse (343:1)'] }, - '18d903735f9c71070b6d8166aa1112f1': { selectors: ['.flex-column-reverse (346:1)'] }, - e2a57aa8196347d4da84f33a4f551325: { selectors: ['.flex-wrap (349:1)'] }, - b6b29f75b174b5609f9e7d5eef457b70: { selectors: ['.flex-nowrap (352:1)'] }, - '839230fc7c0abacb6418b49d8f10b27f': { selectors: ['.flex-wrap-reverse (355:1)'] }, - '924d9b261944a8e8ff684d5b519062bb': { selectors: ['.flex-fill (358:1)'] }, - '5ed396aeb08464b7df8fc37d29455319': { selectors: ['.flex-grow-0 (361:1)'] }, - '94251dc4d012339a3e37df6196fc79bb': { selectors: ['.flex-grow-1 (364:1)'] }, - '0eeed7dabca0452a46574776a4485e6e': { selectors: ['.flex-shrink-0 (367:1)'] }, - c69e60d5e51a1b74d22b172ab98ef9d5: { selectors: ['.flex-shrink-1 (370:1)'] }, - '03401c1a81eb6d4639f020f76dd60176': { selectors: ['.justify-content-start (373:1)'] }, - '39e3d2c344e78869c98ef099249e717d': { selectors: ['.justify-content-end (376:1)'] }, - '3b2d00c0bea857ab78a71b0872842980': { selectors: ['.justify-content-center (379:1)'] }, - b1c3c9edd20ed7c08b43863d38ebee40: { selectors: ['.justify-content-between (382:1)'] }, - a11b4b1d983c5fa75777f273e998f73e: { selectors: ['.justify-content-around (385:1)'] }, - '50e33f29f65bfffa6a3591fcb6045ca9': { selectors: ['.align-items-start (388:1)'] }, - e44b276e47415ec19b74cc16740ada1d: { selectors: ['.align-items-end (391:1)'] }, - '4a9d2716bca651758564059dceed1271': { selectors: ['.align-items-center (394:1)'] }, - fd970b017f7f558f30cb273bf71ede7d: { selectors: ['.align-items-baseline (397:1)'] }, - '7204b6b00b69f8af1e4a24c9b6e7f7f9': { selectors: ['.align-items-stretch (400:1)'] }, - '350fbb74eb70bd05f9438067c3990e9f': { selectors: ['.align-content-start (403:1)'] }, - '74d61397b4fcbf608f3dba39ab3b2a1b': { selectors: ['.align-content-end (406:1)'] }, - eed1ab4ee9e5b327a434512176741548: { selectors: ['.align-content-center (409:1)'] }, - '19878ab832978ef7e1746ac2fe4084b2': { selectors: ['.align-content-between (412:1)'] }, - dded333d0522692809517039f5a727c1: { selectors: ['.align-content-around (415:1)'] }, - '5cbb83ea2af7e9db8ef13f4c7d6db875': { selectors: ['.align-content-stretch (418:1)'] }, - dc3df46d3c023184d375a63a71916646: { selectors: ['.align-self-auto (421:1)'] }, - '0c6d2d8c9732c571f9cf61a4b1d2877f': { selectors: ['.align-self-start (424:1)'] }, - fe7c6071e3e17214df1bdd38850d9ff0: { selectors: ['.align-self-end (427:1)'] }, - '9611bbad74d72d50cf238088576a5089': { selectors: ['.align-self-center (430:1)'] }, - '4bc5edddf5981866946175bfedb7247f': { selectors: ['.align-self-baseline (433:1)'] }, - '4fffdd27ec60120ec9ed16fd7feef801': { selectors: ['.align-self-stretch (436:1)'] }, - '39e658501f5502b35919f02fa9591360': { selectors: ['.float-left (719:1)'] }, - b51436c537ffc4269b1533e44d7c3467: { selectors: ['.float-right (722:1)'] }, - c4597a87d2c793a6d696cfe06f6c95ce: { selectors: ['.float-none (725:1)'] }, - aaf8dc6e0768e59f3098a98a5c144d66: { selectors: ['.position-static (760:1)'] }, - '79592de2383045d15ab57d35aa1dab95': { selectors: ['.position-relative (763:1)'] }, - a7c272f53d0368730bde4c2740ffb5c3: { selectors: ['.position-absolute (766:1)'] }, - dad0bb92d53f17cf8affc10f77824b7f: { selectors: ['.position-fixed (769:1)'] }, - '6da0f6a7354a75fe6c95b08a4cabc06f': { selectors: ['.position-sticky (772:1)'] }, - '66602e21eea7b673883155c8f42b9590': { selectors: ['.fixed-top (775:1)'] }, - '33ea70eb6db7f6ab3469680f182abb19': { selectors: ['.fixed-bottom (782:1)'] }, - '405e9dd7c9919943af14658313fd31e4': { selectors: ['.sr-only (795:1)'] }, - '9220ad156a70c2586b15fe2b9b6108b2': { selectors: ['.shadow-sm (813:1)'] }, - b1b8c0ff70767ca2160811a026766982: { selectors: ['.shadow (816:1)'] }, - da0792abe99964acb6692a03c61d6dd8: { selectors: ['.shadow-lg (819:1)'] }, - bb2173057af1cf20e687469b2d9cbb3c: { selectors: ['.shadow-none (822:1)'] }, - '6d8abb6519a186483b25429ab8b9765e': { selectors: ['.w-25 (825:1)'] }, - a087c1ffdf8ead76cdd37445b414d63e: { selectors: ['.w-50 (828:1)'] }, - '28180742013a90275be5633e6ec0dd51': { selectors: ['.w-75 (831:1)'] }, - '195a03bc95a0af0ba6c8824db97a0b2f': { selectors: ['.w-100 (834:1)'] }, - e67c74b650d6236b03be9dfc10c78e32: { selectors: ['.w-auto (837:1)'] }, - c1b6262b3ee069addc1fbe46f64aac4e: { selectors: ['.h-25 (840:1)'] }, - a520396ae349bef86145e0761aa0699e: { selectors: ['.h-50 (843:1)'] }, - '7c53b57d54beb087fd7ab8b669c5fe60': { selectors: ['.h-75 (846:1)'] }, - ad74f1972cb745b7a78b03e16a387f21: { selectors: ['.h-100 (849:1)'] }, - '2cd49c3d63d260ba4f0b23c559ad05e0': { selectors: ['.h-auto (852:1)'] }, - '0b43071a67efc45ee1735fdc2491313c': { selectors: ['.mw-100 (855:1)'] }, - eac31a6f08e5c935e24b97df0fdad579: { selectors: ['.mh-100 (858:1)'] }, - cfdb4f497b16074959bfd3deb7ea9c42: { selectors: ['.m-0 (861:1)'] }, - '4d666c270ba50524312d97c4b937d153': { selectors: ['.mt-0, .my-0 (864:1)'] }, - eccf47ccd76ceffb4b139cb6c080b5ac: { selectors: ['.mr-0, .mx-0 (868:1)'] }, - '9bc513e73c0bdc6efdf170cb31de16d1': { selectors: ['.mb-0, .my-0 (872:1)'] }, - e99cfc55b03f0e67f11628b19889ad7b: { selectors: ['.ml-0, .mx-0 (876:1)'] }, - e1c39484d90d2acaa00973531f47f738: { selectors: ['.m-1 (880:1)'] }, - '63791bc02eccfdfa2c01621a801e565f': { selectors: ['.mt-1, .my-1 (883:1)'] }, - bcb27ab9d7dcfdd0d7cacad02709966c: { selectors: ['.mr-1, .mx-1 (887:1)'] }, - cb5d1c4328e25b5bc93be9a252973690: { selectors: ['.mb-1, .my-1 (891:1)'] }, - b80b1010c7dcfbb30bed9015c4f2e969: { selectors: ['.ml-1, .mx-1 (895:1)'] }, - ecab1c9cdf8a562e3c0f70307aeafa89: { selectors: ['.m-2 (899:1)'] }, - '6505fe17fbbd88b1884113a754aa82ab': { selectors: ['.mt-2, .my-2 (902:1)'] }, - '6f0c7d09d1e729f332c4671ccc2b48c0': { selectors: ['.mr-2, .mx-2 (906:1)'] }, - '70ef7b668b382b3c747b2d73e08cdbed': { selectors: ['.mb-2, .my-2 (910:1)'] }, - '2d7f277cc78ed324a8fc1f71ab281e1f': { selectors: ['.ml-2, .mx-2 (914:1)'] }, - '8ebcfe52fd4024861082ffb1735747a7': { selectors: ['.m-3 (918:1)'] }, - '9965fb516bdb72b87023a533123a8035': { selectors: ['.mt-3, .my-3 (921:1)'] }, - b1fcbbb1dc6226f6da6000830088e051: { selectors: ['.mr-3, .mx-3 (925:1)'] }, - '02204826cfbe3da98535c0d802870940': { selectors: ['.mb-3, .my-3 (929:1)'] }, - '0259859060250ae6b730218733e7a437': { selectors: ['.ml-3, .mx-3 (933:1)'] }, - '8cf300dab2a4994a105eeddda826f2e6': { selectors: ['.m-4 (937:1)'] }, - '1ba62fdddd3349f52a452050688905c7': { selectors: ['.mt-4, .my-4 (940:1)'] }, - '66a104129fa13db5a0829567fba6ee41': { selectors: ['.mr-4, .mx-4 (944:1)'] }, - eefcc4c10b79e70e8e8a5a66fb2b7aa1: { selectors: ['.mb-4, .my-4 (948:1)'] }, - eb1503656dc920d15a31116956fdffa4: { selectors: ['.ml-4, .mx-4 (952:1)'] }, - '79cbb6e5c9b73fd0be29d4fc5733a099': { selectors: ['.m-5 (956:1)'] }, - '67d8671699df706a428e7da42a7141cb': { selectors: ['.mt-5, .my-5 (959:1)'] }, - e9cb4a0a8a60ff018c87a0b7efa9de29: { selectors: ['.mr-5, .mx-5 (963:1)'] }, - '93f579214354dbd8cb60209c068f0086': { selectors: ['.mb-5, .my-5 (967:1)'] }, - '2a789d4af97d2b87fd0bf2b4626120cd': { selectors: ['.ml-5, .mx-5 (971:1)'] }, - '64a89d28e8287c1a0ac153001082644c': { selectors: ['.p-0 (975:1)'] }, - b03aa6db5ddf110bbdbefbbec43fda30: { selectors: ['.pt-0, .py-0 (978:1)'] }, - e38192ca32a98888d4c4876880f4fece: { selectors: ['.pr-0, .px-0 (982:1)'] }, - '70fe8ef50e999ddd29506f672c107069': { selectors: ['.pb-0, .py-0 (986:1)'] }, - '9355e8cd9109049726475ba356661bcf': { selectors: ['.pl-0, .px-0 (990:1)'] }, - '0d4c53468c2658c5324b9ec7a8ca6de2': { selectors: ['.p-1 (994:1)'] }, - d74e430b2a56b3a4e20065c972b7fa3f: { selectors: ['.pt-1, .py-1 (997:1)'] }, - '21e4644967aedd19888b6f4a700b629b': { selectors: ['.pr-1, .px-1 (1001:1)'] }, - e315a7b9b7a1d0df3ea7d95af5203a0b: { selectors: ['.pb-1, .py-1 (1005:1)'] }, - '14630ca122e1d9830a9ef5591c4097d0': { selectors: ['.pl-1, .px-1 (1009:1)'] }, - '5b1c65e5139e86e5f4755824f8b77d13': { selectors: ['.p-2 (1013:1)'] }, - '244af70950a1e200d3849f75ce51d707': { selectors: ['.pt-2, .py-2 (1016:1)'] }, - b583832738cad724c7c23e5c14ac9bfb: { selectors: ['.pr-2, .px-2 (1020:1)'] }, - e1e633c4f1375e8276154192d8899e39: { selectors: ['.pb-2, .py-2 (1024:1)'] }, - '676b01e25f0dbb3f7d2f2529231cda08': { selectors: ['.pl-2, .px-2 (1028:1)'] }, - '9b5165e3333b22801f2287f7983d7516': { selectors: ['.p-3 (1032:1)'] }, - '5bcaa9df87a507f6cd14659ea176bdc5': { selectors: ['.pt-3, .py-3 (1035:1)'] }, - f706637180776c5589385599705a2409: { selectors: ['.pr-3, .px-3 (1039:1)'] }, - '41157cfbcf47990b383b5b0379386ab2': { selectors: ['.pb-3, .py-3 (1043:1)'] }, - cac1e7a204bb6a1f42707b684ad46238: { selectors: ['.pl-3, .px-3 (1047:1)'] }, - '43e0671cd41a4b7590284888b607a134': { selectors: ['.p-4 (1051:1)'] }, - '116b0f95ebde1ff8907e488413a88854': { selectors: ['.pt-4, .py-4 (1054:1)'] }, - ecb06765fe691d892df000eebbb23dcc: { selectors: ['.pr-4, .px-4 (1058:1)'] }, - '1331503a48d36025c861e660bc615048': { selectors: ['.pb-4, .py-4 (1062:1)'] }, - f8665f7e547e499abd7ac63813b274f5: { selectors: ['.pl-4, .px-4 (1066:1)'] }, - '4160a315459f1b5a98255863f42136fe': { selectors: ['.p-5 (1070:1)'] }, - f55a6b2de6a434ec7b4375f06f4fad75: { selectors: ['.pt-5, .py-5 (1073:1)'] }, - '19391dc45c8d7730a86d521c28f52c3f': { selectors: ['.pr-5, .px-5 (1077:1)'] }, - '15898bcb7ff74a60006f9931422b4ad3': { selectors: ['.pb-5, .py-5 (1081:1)'] }, - '6290bdc6355aed1e9b27379003aa4828': { selectors: ['.pl-5, .px-5 (1085:1)'] }, - e57ec4fe9e8ed36e38f1c50041fc9f47: { selectors: ['.m-auto (1089:1)'] }, - f10380665932186d1effe0674a74ba12: { selectors: ['.mt-auto, .my-auto (1092:1)'] }, - '2ce71a27023eb50a47c24a99399faa28': { selectors: ['.mr-auto, .mx-auto (1096:1)'] }, - '196c77d357d314678cd3a99cfacbea96': { selectors: ['.mb-auto, .my-auto (1100:1)'] }, - ca007ce268b463a6bf42145cf5ce3685: { selectors: ['.ml-auto, .mx-auto (1104:1)'] }, - cb431b84034f2e710509c7656b3c6f16: { selectors: ['.text-monospace (1844:1)'] }, - a8fc5ca823f51d72673577064387a029: { selectors: ['.text-justify (1847:1)'] }, - '0bb94dfab7ca2c9892ebbd993b2baf0f': { selectors: ['.text-nowrap (1850:1)'] }, - aea4958ce85ddc0cbffca1015c3a7eba: { selectors: ['.text-truncate (1853:1)'] }, - '52b9443947b6b94a5c7e1b837da115e2': { selectors: ['.text-left (1858:1)'] }, - baaf5136fc6e1c54ba29b6040f166d5f: { selectors: ['.text-right (1861:1)'] }, - '282aa4319bee75af06cc2632b7124e26': { selectors: ['.text-center (1864:1)'] }, - '1cb1c8ad9b560eca25ebcefe95c1b7fa': { selectors: ['.text-lowercase (1899:1)'] }, - '45234533eac658ba2857e9c4d3bc78a5': { selectors: ['.text-uppercase (1902:1)'] }, - f9e3f64237f2e81b6aed84223a0ceb1d: { selectors: ['.text-capitalize (1905:1)'] }, - '09caca3d36aa9f3ef815e0da7e1a16b4': { selectors: ['.font-weight-light (1908:1)'] }, - '25189f4fad18eaeef19e349c6680834c': { selectors: ['.font-weight-normal (1911:1)'] }, - b2a9507678ec557603eb8ec077f0eb1f: { selectors: ['.font-weight-bold (1914:1)'] }, - '7d2da06b621a98a8599e5ec82e39eac8': { selectors: ['.font-italic (1917:1)'] }, - '0020d10e4fce033b418aace7c3143b82': { selectors: ['.text-white (1920:1)'] }, - '34ad81e372a038e6f78ae4f22bd4813d': { selectors: ['.text-primary (1923:1)'] }, + c5009af89633c4d2f71a0a9fa333630d: { selectors: ['.flex-row (501:1)'] }, + '7b06a3d956579cd64b6f5b1a57255369': { selectors: ['.flex-column (505:1)'] }, + '21744a8c4dc6ed1519903b4236b00af4': { selectors: ['.flex-row-reverse (509:1)'] }, + '18d903735f9c71070b6d8166aa1112f1': { selectors: ['.flex-column-reverse (513:1)'] }, + e2a57aa8196347d4da84f33a4f551325: { selectors: ['.flex-wrap (517:1)'] }, + b6b29f75b174b5609f9e7d5eef457b70: { selectors: ['.flex-nowrap (521:1)'] }, + '839230fc7c0abacb6418b49d8f10b27f': { selectors: ['.flex-wrap-reverse (525:1)'] }, + '924d9b261944a8e8ff684d5b519062bb': { selectors: ['.flex-fill (529:1)'] }, + '5ed396aeb08464b7df8fc37d29455319': { selectors: ['.flex-grow-0 (533:1)'] }, + '94251dc4d012339a3e37df6196fc79bb': { selectors: ['.flex-grow-1 (537:1)'] }, + '0eeed7dabca0452a46574776a4485e6e': { selectors: ['.flex-shrink-0 (541:1)'] }, + c69e60d5e51a1b74d22b172ab98ef9d5: { selectors: ['.flex-shrink-1 (545:1)'] }, + '03401c1a81eb6d4639f020f76dd60176': { selectors: ['.justify-content-start (549:1)'] }, + '39e3d2c344e78869c98ef099249e717d': { selectors: ['.justify-content-end (553:1)'] }, + '3b2d00c0bea857ab78a71b0872842980': { selectors: ['.justify-content-center (557:1)'] }, + b1c3c9edd20ed7c08b43863d38ebee40: { selectors: ['.justify-content-between (561:1)'] }, + a11b4b1d983c5fa75777f273e998f73e: { selectors: ['.justify-content-around (565:1)'] }, + '50e33f29f65bfffa6a3591fcb6045ca9': { selectors: ['.align-items-start (569:1)'] }, + e44b276e47415ec19b74cc16740ada1d: { selectors: ['.align-items-end (573:1)'] }, + '4a9d2716bca651758564059dceed1271': { selectors: ['.align-items-center (577:1)'] }, + fd970b017f7f558f30cb273bf71ede7d: { selectors: ['.align-items-baseline (581:1)'] }, + '7204b6b00b69f8af1e4a24c9b6e7f7f9': { selectors: ['.align-items-stretch (585:1)'] }, + '350fbb74eb70bd05f9438067c3990e9f': { selectors: ['.align-content-start (589:1)'] }, + '74d61397b4fcbf608f3dba39ab3b2a1b': { selectors: ['.align-content-end (593:1)'] }, + eed1ab4ee9e5b327a434512176741548: { selectors: ['.align-content-center (597:1)'] }, + '19878ab832978ef7e1746ac2fe4084b2': { selectors: ['.align-content-between (601:1)'] }, + dded333d0522692809517039f5a727c1: { selectors: ['.align-content-around (605:1)'] }, + '5cbb83ea2af7e9db8ef13f4c7d6db875': { selectors: ['.align-content-stretch (609:1)'] }, + dc3df46d3c023184d375a63a71916646: { selectors: ['.align-self-auto (613:1)'] }, + '0c6d2d8c9732c571f9cf61a4b1d2877f': { selectors: ['.align-self-start (617:1)'] }, + fe7c6071e3e17214df1bdd38850d9ff0: { selectors: ['.align-self-end (621:1)'] }, + '9611bbad74d72d50cf238088576a5089': { selectors: ['.align-self-center (625:1)'] }, + '4bc5edddf5981866946175bfedb7247f': { selectors: ['.align-self-baseline (629:1)'] }, + '4fffdd27ec60120ec9ed16fd7feef801': { selectors: ['.align-self-stretch (633:1)'] }, + '39e658501f5502b35919f02fa9591360': { selectors: ['.float-left (1185:1)'] }, + b51436c537ffc4269b1533e44d7c3467: { selectors: ['.float-right (1189:1)'] }, + c4597a87d2c793a6d696cfe06f6c95ce: { selectors: ['.float-none (1193:1)'] }, + '16b2e7e5b666b69b9581be6a44947d6f': { selectors: ['.user-select-all (1249:1)'] }, + f5c4d8f25b8338031e719edc77659558: { selectors: ['.user-select-auto (1253:1)'] }, + '71ba65d43dcc2b1c3f47ac2578983e83': { selectors: ['.user-select-none (1257:1)'] }, + '5902adc842d83ad70b89ece99b82a7a9': { selectors: ['.overflow-auto (1261:1)'] }, + bfb19f18b91fa46f0502e32153287cc4: { selectors: ['.overflow-hidden (1265:1)'] }, + aaf8dc6e0768e59f3098a98a5c144d66: { selectors: ['.position-static (1269:1)'] }, + '79592de2383045d15ab57d35aa1dab95': { selectors: ['.position-relative (1273:1)'] }, + a7c272f53d0368730bde4c2740ffb5c3: { selectors: ['.position-absolute (1277:1)'] }, + dad0bb92d53f17cf8affc10f77824b7f: { selectors: ['.position-fixed (1281:1)'] }, + '6da0f6a7354a75fe6c95b08a4cabc06f': { selectors: ['.position-sticky (1285:1)'] }, + '66602e21eea7b673883155c8f42b9590': { selectors: ['.fixed-top (1289:1)'] }, + '33ea70eb6db7f6ab3469680f182abb19': { selectors: ['.fixed-bottom (1297:1)'] }, + '947009e63b4547795ef7cc873f4e4ae9': { selectors: ['.sr-only (1313:1)'] }, + '9220ad156a70c2586b15fe2b9b6108b2': { selectors: ['.shadow-sm (1334:1)'] }, + b1b8c0ff70767ca2160811a026766982: { selectors: ['.shadow (1338:1)'] }, + da0792abe99964acb6692a03c61d6dd8: { selectors: ['.shadow-lg (1342:1)'] }, + bb2173057af1cf20e687469b2d9cbb3c: { selectors: ['.shadow-none (1346:1)'] }, + '6d8abb6519a186483b25429ab8b9765e': { selectors: ['.w-25 (1350:1)'] }, + a087c1ffdf8ead76cdd37445b414d63e: { selectors: ['.w-50 (1354:1)'] }, + '28180742013a90275be5633e6ec0dd51': { selectors: ['.w-75 (1358:1)'] }, + '195a03bc95a0af0ba6c8824db97a0b2f': { selectors: ['.w-100 (1362:1)'] }, + e67c74b650d6236b03be9dfc10c78e32: { selectors: ['.w-auto (1366:1)'] }, + c1b6262b3ee069addc1fbe46f64aac4e: { selectors: ['.h-25 (1370:1)'] }, + a520396ae349bef86145e0761aa0699e: { selectors: ['.h-50 (1374:1)'] }, + '7c53b57d54beb087fd7ab8b669c5fe60': { selectors: ['.h-75 (1378:1)'] }, + ad74f1972cb745b7a78b03e16a387f21: { selectors: ['.h-100 (1382:1)'] }, + '2cd49c3d63d260ba4f0b23c559ad05e0': { selectors: ['.h-auto (1386:1)'] }, + '0b43071a67efc45ee1735fdc2491313c': { selectors: ['.mw-100 (1390:1)'] }, + eac31a6f08e5c935e24b97df0fdad579: { selectors: ['.mh-100 (1394:1)'] }, + bd5f5d687d100d127abeb044fc78a3fd: { selectors: ['.min-vw-100 (1398:1)'] }, + '8dec7662784b943d3ca0615df59d7970': { selectors: ['.min-vh-100 (1402:1)'] }, + eacc17f16197f6c0291ced3e7cfc067e: { selectors: ['.vw-100 (1406:1)'] }, + '1a2ec1c9ad7db8d4156ca76a81416c49': { selectors: ['.vh-100 (1410:1)'] }, + cfdb4f497b16074959bfd3deb7ea9c42: { selectors: ['.m-0 (1414:1)'] }, + '4d666c270ba50524312d97c4b937d153': { selectors: ['.mt-0, .my-0 (1418:1)'] }, + eccf47ccd76ceffb4b139cb6c080b5ac: { selectors: ['.mr-0, .mx-0 (1423:1)'] }, + '9bc513e73c0bdc6efdf170cb31de16d1': { selectors: ['.mb-0, .my-0 (1428:1)'] }, + e99cfc55b03f0e67f11628b19889ad7b: { selectors: ['.ml-0, .mx-0 (1433:1)'] }, + e1c39484d90d2acaa00973531f47f738: { selectors: ['.m-1 (1438:1)'] }, + '63791bc02eccfdfa2c01621a801e565f': { selectors: ['.mt-1, .my-1 (1442:1)'] }, + bcb27ab9d7dcfdd0d7cacad02709966c: { selectors: ['.mr-1, .mx-1 (1447:1)'] }, + cb5d1c4328e25b5bc93be9a252973690: { selectors: ['.mb-1, .my-1 (1452:1)'] }, + b80b1010c7dcfbb30bed9015c4f2e969: { selectors: ['.ml-1, .mx-1 (1457:1)'] }, + ecab1c9cdf8a562e3c0f70307aeafa89: { selectors: ['.m-2 (1462:1)'] }, + '6505fe17fbbd88b1884113a754aa82ab': { selectors: ['.mt-2, .my-2 (1466:1)'] }, + '6f0c7d09d1e729f332c4671ccc2b48c0': { selectors: ['.mr-2, .mx-2 (1471:1)'] }, + '70ef7b668b382b3c747b2d73e08cdbed': { selectors: ['.mb-2, .my-2 (1476:1)'] }, + '2d7f277cc78ed324a8fc1f71ab281e1f': { selectors: ['.ml-2, .mx-2 (1481:1)'] }, + '8ebcfe52fd4024861082ffb1735747a7': { selectors: ['.m-3 (1486:1)'] }, + '9965fb516bdb72b87023a533123a8035': { selectors: ['.mt-3, .my-3 (1490:1)'] }, + b1fcbbb1dc6226f6da6000830088e051: { selectors: ['.mr-3, .mx-3 (1495:1)'] }, + '02204826cfbe3da98535c0d802870940': { selectors: ['.mb-3, .my-3 (1500:1)'] }, + '0259859060250ae6b730218733e7a437': { selectors: ['.ml-3, .mx-3 (1505:1)'] }, + '8cf300dab2a4994a105eeddda826f2e6': { selectors: ['.m-4 (1510:1)'] }, + '1ba62fdddd3349f52a452050688905c7': { selectors: ['.mt-4, .my-4 (1514:1)'] }, + '66a104129fa13db5a0829567fba6ee41': { selectors: ['.mr-4, .mx-4 (1519:1)'] }, + eefcc4c10b79e70e8e8a5a66fb2b7aa1: { selectors: ['.mb-4, .my-4 (1524:1)'] }, + eb1503656dc920d15a31116956fdffa4: { selectors: ['.ml-4, .mx-4 (1529:1)'] }, + '79cbb6e5c9b73fd0be29d4fc5733a099': { selectors: ['.m-5 (1534:1)'] }, + '67d8671699df706a428e7da42a7141cb': { selectors: ['.mt-5, .my-5 (1538:1)'] }, + e9cb4a0a8a60ff018c87a0b7efa9de29: { selectors: ['.mr-5, .mx-5 (1543:1)'] }, + '93f579214354dbd8cb60209c068f0086': { selectors: ['.mb-5, .my-5 (1548:1)'] }, + '2a789d4af97d2b87fd0bf2b4626120cd': { selectors: ['.ml-5, .mx-5 (1553:1)'] }, + '64a89d28e8287c1a0ac153001082644c': { selectors: ['.p-0 (1558:1)'] }, + b03aa6db5ddf110bbdbefbbec43fda30: { selectors: ['.pt-0, .py-0 (1562:1)'] }, + e38192ca32a98888d4c4876880f4fece: { selectors: ['.pr-0, .px-0 (1567:1)'] }, + '70fe8ef50e999ddd29506f672c107069': { selectors: ['.pb-0, .py-0 (1572:1)'] }, + '9355e8cd9109049726475ba356661bcf': { selectors: ['.pl-0, .px-0 (1577:1)'] }, + '0d4c53468c2658c5324b9ec7a8ca6de2': { selectors: ['.p-1 (1582:1)'] }, + d74e430b2a56b3a4e20065c972b7fa3f: { selectors: ['.pt-1, .py-1 (1586:1)'] }, + '21e4644967aedd19888b6f4a700b629b': { selectors: ['.pr-1, .px-1 (1591:1)'] }, + e315a7b9b7a1d0df3ea7d95af5203a0b: { selectors: ['.pb-1, .py-1 (1596:1)'] }, + '14630ca122e1d9830a9ef5591c4097d0': { selectors: ['.pl-1, .px-1 (1601:1)'] }, + '5b1c65e5139e86e5f4755824f8b77d13': { selectors: ['.p-2 (1606:1)'] }, + '244af70950a1e200d3849f75ce51d707': { selectors: ['.pt-2, .py-2 (1610:1)'] }, + b583832738cad724c7c23e5c14ac9bfb: { selectors: ['.pr-2, .px-2 (1615:1)'] }, + e1e633c4f1375e8276154192d8899e39: { selectors: ['.pb-2, .py-2 (1620:1)'] }, + '676b01e25f0dbb3f7d2f2529231cda08': { selectors: ['.pl-2, .px-2 (1625:1)'] }, + '9b5165e3333b22801f2287f7983d7516': { selectors: ['.p-3 (1630:1)'] }, + '5bcaa9df87a507f6cd14659ea176bdc5': { selectors: ['.pt-3, .py-3 (1634:1)'] }, + f706637180776c5589385599705a2409: { selectors: ['.pr-3, .px-3 (1639:1)'] }, + '41157cfbcf47990b383b5b0379386ab2': { selectors: ['.pb-3, .py-3 (1644:1)'] }, + cac1e7a204bb6a1f42707b684ad46238: { selectors: ['.pl-3, .px-3 (1649:1)'] }, + '43e0671cd41a4b7590284888b607a134': { selectors: ['.p-4 (1654:1)'] }, + '116b0f95ebde1ff8907e488413a88854': { selectors: ['.pt-4, .py-4 (1658:1)'] }, + ecb06765fe691d892df000eebbb23dcc: { selectors: ['.pr-4, .px-4 (1663:1)'] }, + '1331503a48d36025c861e660bc615048': { selectors: ['.pb-4, .py-4 (1668:1)'] }, + f8665f7e547e499abd7ac63813b274f5: { selectors: ['.pl-4, .px-4 (1673:1)'] }, + '4160a315459f1b5a98255863f42136fe': { selectors: ['.p-5 (1678:1)'] }, + f55a6b2de6a434ec7b4375f06f4fad75: { selectors: ['.pt-5, .py-5 (1682:1)'] }, + '19391dc45c8d7730a86d521c28f52c3f': { selectors: ['.pr-5, .px-5 (1687:1)'] }, + '15898bcb7ff74a60006f9931422b4ad3': { selectors: ['.pb-5, .py-5 (1692:1)'] }, + '6290bdc6355aed1e9b27379003aa4828': { selectors: ['.pl-5, .px-5 (1697:1)'] }, + '04a144abd49b1402e3588fb4ec237ec0': { selectors: ['.m-n1 (1702:1)'] }, + b81793fb475f7cc3a0698aa92ae41777: { selectors: ['.mt-n1, .my-n1 (1706:1)'] }, + '3c39187fca578a65e7aa2d704208cde8': { selectors: ['.mr-n1, .mx-n1 (1711:1)'] }, + c6c24440dd9edef71c7bc7da951dccd5: { selectors: ['.mb-n1, .my-n1 (1716:1)'] }, + '8b528f327ea3e562fc61d13c9f3901d8': { selectors: ['.ml-n1, .mx-n1 (1721:1)'] }, + '4a1926d211f81e5c96d4c9c4f2097774': { selectors: ['.m-n2 (1726:1)'] }, + f78f1986440a943aeb5553cb11b2f23c: { selectors: ['.mt-n2, .my-n2 (1730:1)'] }, + '941a9ea73cc97224a3f55ef65ab727c9': { selectors: ['.mr-n2, .mx-n2 (1735:1)'] }, + '074441007089a0c89cda9bfcb31c763a': { selectors: ['.mb-n2, .my-n2 (1740:1)'] }, + '7f9e45f6286b38eec32d471b7c1b28b6': { selectors: ['.ml-n2, .mx-n2 (1745:1)'] }, + c322ab87c4e181d835e6c17a1ca179ad: { selectors: ['.m-n3 (1750:1)'] }, + dee7caca54972db68f956a7cfd5d372b: { selectors: ['.mt-n3, .my-n3 (1754:1)'] }, + '60c8b6aa0702bbc3eed911f7483876ce': { selectors: ['.mr-n3, .mx-n3 (1759:1)'] }, + '595290cc6428614c03bac4631fe3fc12': { selectors: ['.mb-n3, .my-n3 (1764:1)'] }, + '7b968bc3150d9768117e099261cf23df': { selectors: ['.ml-n3, .mx-n3 (1769:1)'] }, + '90ca4942944fbc4dd543a821244daa31': { selectors: ['.m-n4 (1774:1)'] }, + '1716842c880f907fc62889f928273e0c': { selectors: ['.mt-n4, .my-n4 (1778:1)'] }, + '087ab1596162227935a974af5e856174': { selectors: ['.mr-n4, .mx-n4 (1783:1)'] }, + f53133714696eaf034f3efe057ac82fe: { selectors: ['.mb-n4, .my-n4 (1788:1)'] }, + ce56811b70ca493d65e3f63acfa2d9df: { selectors: ['.ml-n4, .mx-n4 (1793:1)'] }, + '7f52135983abe2eccfbb72e5d16a6a20': { selectors: ['.m-n5 (1798:1)'] }, + de8fc351c8c23bd867cf10a14642899a: { selectors: ['.mt-n5, .my-n5 (1802:1)'] }, + b134cecaa2883916d516ae6a2b1a3891: { selectors: ['.mr-n5, .mx-n5 (1807:1)'] }, + '124ccc6a210fadb889b2742eb0f9b540': { selectors: ['.mb-n5, .my-n5 (1812:1)'] }, + c3948b63b7b0c864f4f78faf640fd9a0: { selectors: ['.ml-n5, .mx-n5 (1817:1)'] }, + e57ec4fe9e8ed36e38f1c50041fc9f47: { selectors: ['.m-auto (1822:1)'] }, + f10380665932186d1effe0674a74ba12: { selectors: ['.mt-auto, .my-auto (1826:1)'] }, + '2ce71a27023eb50a47c24a99399faa28': { selectors: ['.mr-auto, .mx-auto (1831:1)'] }, + '196c77d357d314678cd3a99cfacbea96': { selectors: ['.mb-auto, .my-auto (1836:1)'] }, + ca007ce268b463a6bf42145cf5ce3685: { selectors: ['.ml-auto, .mx-auto (1841:1)'] }, + '695821b1faf108a559486730b246876a': { selectors: ['.text-monospace (3590:1)'] }, + a8fc5ca823f51d72673577064387a029: { selectors: ['.text-justify (3594:1)'] }, + '04e83ef005e9c653fb368008f8ae8a33': { selectors: ['.text-wrap (3598:1)'] }, + '0bb94dfab7ca2c9892ebbd993b2baf0f': { selectors: ['.text-nowrap (3602:1)'] }, + aea4958ce85ddc0cbffca1015c3a7eba: { selectors: ['.text-truncate (3606:1)'] }, + '52b9443947b6b94a5c7e1b837da115e2': { selectors: ['.text-left (3612:1)'] }, + baaf5136fc6e1c54ba29b6040f166d5f: { selectors: ['.text-right (3616:1)'] }, + '282aa4319bee75af06cc2632b7124e26': { selectors: ['.text-center (3620:1)'] }, + '1cb1c8ad9b560eca25ebcefe95c1b7fa': { selectors: ['.text-lowercase (3676:1)'] }, + '45234533eac658ba2857e9c4d3bc78a5': { selectors: ['.text-uppercase (3680:1)'] }, + f9e3f64237f2e81b6aed84223a0ceb1d: { selectors: ['.text-capitalize (3684:1)'] }, + '09caca3d36aa9f3ef815e0da7e1a16b4': { selectors: ['.font-weight-light (3688:1)'] }, + '23544682bb214a76e383e032d4056be4': { selectors: ['.font-weight-lighter (3692:1)'] }, + '25189f4fad18eaeef19e349c6680834c': { selectors: ['.font-weight-normal (3696:1)'] }, + b2a9507678ec557603eb8ec077f0eb1f: { selectors: ['.font-weight-bold (3700:1)'] }, + '9d05a94a577558235edce4c8fd9ab639': { selectors: ['.font-weight-bolder (3704:1)'] }, + '7d2da06b621a98a8599e5ec82e39eac8': { selectors: ['.font-italic (3708:1)'] }, + '0020d10e4fce033b418aace7c3143b82': { selectors: ['.text-white (3712:1)'] }, + '34ad81e372a038e6f78ae4f22bd4813d': { selectors: ['.text-primary (3716:1)'] }, '9fde9a179d24755438ace2a874dda817': { - selectors: ['.text-secondary (1929:1)', '.text-muted (1974:1)'], + selectors: ['.text-secondary (3724:1)', '.text-muted (3784:1)'], }, - '9ffcb1532b3fb397c0e818850683da29': { selectors: ['.text-success (1935:1)'] }, - f28fd089809bcd15d5684b158a0af98d: { selectors: ['.text-info (1941:1)'] }, - '6cac1cb5ee5149e91e45d15d0bdae310': { selectors: ['.text-warning (1947:1)'] }, - '2faab1e0abf22b20fdf05b9b01fff29b': { selectors: ['.text-danger (1953:1)'] }, - '46b52fea531aaaf29b63c40be2356849': { selectors: ['.text-light (1959:1)'] }, - '78f31d1ab6529decf28e0366a8ee81aa': { selectors: ['.text-dark (1965:1)'] }, - '45330b41b77e8880ad7680c51e0f61c4': { selectors: ['.text-body (1971:1)'] }, - '60d93588f62b5e85eb4f11dfd3461897': { selectors: ['.text-black-50 (1977:1)'] }, - '7dea35658553032ff7b7cc0287613b7c': { selectors: ['.text-white-50 (1980:1)'] }, - '61bf92980cac3d51d0cf1ba24c948fa1': { selectors: ['.text-hide (1983:1)'] }, - '7dcad258820769677bc60871fafe9b93': { selectors: ['.visible (1990:1)'] }, - '0f8833af4e2f4a6fc785bd7edc1e75b3': { selectors: ['.invisible (1993:1)'] }, + '9ffcb1532b3fb397c0e818850683da29': { selectors: ['.text-success (3732:1)'] }, + f28fd089809bcd15d5684b158a0af98d: { selectors: ['.text-info (3740:1)'] }, + '6cac1cb5ee5149e91e45d15d0bdae310': { selectors: ['.text-warning (3748:1)'] }, + '2faab1e0abf22b20fdf05b9b01fff29b': { selectors: ['.text-danger (3756:1)'] }, + '46b52fea531aaaf29b63c40be2356849': { selectors: ['.text-light (3764:1)'] }, + '78f31d1ab6529decf28e0366a8ee81aa': { selectors: ['.text-dark (3772:1)'] }, + '45330b41b77e8880ad7680c51e0f61c4': { selectors: ['.text-body (3780:1)'] }, + '60d93588f62b5e85eb4f11dfd3461897': { selectors: ['.text-black-50 (3788:1)'] }, + '7dea35658553032ff7b7cc0287613b7c': { selectors: ['.text-white-50 (3792:1)'] }, + '61bf92980cac3d51d0cf1ba24c948fa1': { selectors: ['.text-hide (3796:1)'] }, + '187bfde85cb4d8bdaa41735f54f5c4f8': { selectors: ['.text-decoration-none (3804:1)'] }, + fd57878967b8dce947091cdc114efd83: { selectors: ['.text-break (3808:1)'] }, + '43b8d67240c3b09505d94522a62a977c': { selectors: ['.text-reset (3813:1)'] }, + '7dcad258820769677bc60871fafe9b93': { selectors: ['.visible (3817:1)'] }, + '0f8833af4e2f4a6fc785bd7edc1e75b3': { selectors: ['.invisible (3821:1)'] }, }; diff --git a/spec/controllers/registrations/experience_levels_controller_spec.rb b/spec/controllers/registrations/experience_levels_controller_spec.rb index 79fa3f1474a..6b8ab3ec715 100644 --- a/spec/controllers/registrations/experience_levels_controller_spec.rb +++ b/spec/controllers/registrations/experience_levels_controller_spec.rb @@ -76,7 +76,7 @@ RSpec.describe Registrations::ExperienceLevelsController do let(:learn_gitlab_available?) { true } before do - allow_next_instance_of(LearnGitlab) do |learn_gitlab| + allow_next_instance_of(LearnGitlab::Project) do |learn_gitlab| allow(learn_gitlab).to receive(:available?).and_return(learn_gitlab_available?) allow(learn_gitlab).to receive(:project).and_return(project) allow(learn_gitlab).to receive(:board).and_return(issues_board) @@ -136,7 +136,7 @@ RSpec.describe Registrations::ExperienceLevelsController do let(:params) { super().merge(experience_level: :novice) } before do - allow_next(LearnGitlab).to receive(:available?).and_return(false) + allow_next(LearnGitlab::Project).to receive(:available?).and_return(false) end it 'does not add a BoardLabel' do diff --git a/spec/factories/bulk_import/export_uploads.rb b/spec/factories/bulk_import/export_uploads.rb new file mode 100644 index 00000000000..9f03498b9d9 --- /dev/null +++ b/spec/factories/bulk_import/export_uploads.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :bulk_import_export_upload, class: 'BulkImports::ExportUpload' do + export { association(:bulk_import_export) } + end +end diff --git a/spec/factories/bulk_import/exports.rb b/spec/factories/bulk_import/exports.rb new file mode 100644 index 00000000000..dd8831ce33a --- /dev/null +++ b/spec/factories/bulk_import/exports.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :bulk_import_export, class: 'BulkImports::Export', traits: %i[started] do + group + relation { 'labels' } + + trait :started do + status { 0 } + + sequence(:jid) { |n| "bulk_import_export_#{n}" } + end + + trait :finished do + status { 1 } + + sequence(:jid) { |n| "bulk_import_export_#{n}" } + end + + trait :failed do + status { -1 } + end + end +end diff --git a/spec/factories/packages/package_file.rb b/spec/factories/packages/package_file.rb index 74400975670..1ce41f3d7b8 100644 --- a/spec/factories/packages/package_file.rb +++ b/spec/factories/packages/package_file.rb @@ -205,6 +205,8 @@ FactoryBot.define do file_fixture { 'spec/fixtures/packages/maven/my-app-1.0-20180724.124855-1.jar' } file_name { 'my-app-1.0-20180724.124855-1.jar' } file_sha1 { '4f0bfa298744d505383fbb57c554d4f5c12d88b3' } + file_md5 { '0a7392d24f42f83068fa3767c5310052' } + file_sha256 { '440e5e148a25331bbd7991575f7d54933c0ebf6cc735a18ee5066ac1381bb590' } size { 100.kilobytes } end @@ -212,6 +214,8 @@ FactoryBot.define do file_fixture { 'spec/fixtures/packages/maven/my-app-1.0-20180724.124855-1.pom' } file_name { 'my-app-1.0-20180724.124855-1.pom' } file_sha1 { '19c975abd49e5102ca6c74a619f21e0cf0351c57' } + file_md5 { '0a7392d24f42f83068fa3767c5310052' } + file_sha256 { '440e5e148a25331bbd7991575f7d54933c0ebf6cc735a18ee5066ac1381bb590' } size { 200.kilobytes } end @@ -219,6 +223,8 @@ FactoryBot.define do file_fixture { 'spec/fixtures/packages/maven/maven-metadata.xml' } file_name { 'maven-metadata.xml' } file_sha1 { '42b1bdc80de64953b6876f5a8c644f20204011b0' } + file_md5 { '0a7392d24f42f83068fa3767c5310052' } + file_sha256 { '440e5e148a25331bbd7991575f7d54933c0ebf6cc735a18ee5066ac1381bb590' } size { 300.kilobytes } end diff --git a/spec/features/merge_request/user_sees_merge_widget_spec.rb b/spec/features/merge_request/user_sees_merge_widget_spec.rb index e965fc94682..d9b5ec17a4a 100644 --- a/spec/features/merge_request/user_sees_merge_widget_spec.rb +++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb @@ -445,7 +445,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js do wait_for_requests expect(page).not_to have_button('Merge') - expect(page).to have_content('Merging! Drum roll, please…') + expect(page).to have_content('Merging!') end end diff --git a/spec/fixtures/api/schemas/public_api/v4/packages/package_files.json b/spec/fixtures/api/schemas/public_api/v4/packages/package_files.json index 93b6dcde080..332e42fdbe5 100644 --- a/spec/fixtures/api/schemas/public_api/v4/packages/package_files.json +++ b/spec/fixtures/api/schemas/public_api/v4/packages/package_files.json @@ -8,6 +8,8 @@ "package_id": { "type": "integer" }, "file_name": { "type": "string" }, "file_sha1": { "type": "string" }, + "file_sha256": { "type": "string" }, + "file_md5": { "type": "string" }, "pipelines": { "items": { "$ref": "../pipeline.json" } } diff --git a/spec/fixtures/api/schemas/public_api/v4/project.json b/spec/fixtures/api/schemas/public_api/v4/project.json new file mode 100644 index 00000000000..4a3149f2bdc --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/project.json @@ -0,0 +1,45 @@ +{ + "type": "object", + "properties" : { + "id": { "type": "integer" }, + "name": { "type": "string" }, + "name_with_namespace": { "type": "string" }, + "description": { "type": ["string", "null"] }, + "path": { "type": "string" }, + "path_with_namespace": { "type": "string" }, + "created_at": { "type": "string", "format": "date-time" }, + "default_branch": { "type": ["string", "null"] }, + "tag_list": { + "type": "array", + "items": { + "type": "string" + } + }, + "ssh_url_to_repo": { "type": "string" }, + "http_url_to_repo": { "type": "string" }, + "web_url": { "type": "string" }, + "readme_url": { "type": ["string", "null"] }, + "avatar_url": { "type": ["string", "null"] }, + "star_count": { "type": "integer" }, + "forks_count": { "type": "integer" }, + "last_activity_at": { "type": "string", "format": "date-time" }, + "namespace": { + "type": "object", + "properties" : { + "id": { "type": "integer" }, + "name": { "type": "string" }, + "path": { "type": "string" }, + "kind": { "type": "string" }, + "full_path": { "type": "string" }, + "parent_id": { "type": ["integer", "null"] } + } + } + }, + "required": [ + "id", "name", "name_with_namespace", "description", "path", + "path_with_namespace", "created_at", "default_branch", "tag_list", + "ssh_url_to_repo", "http_url_to_repo", "web_url", "readme_url", "avatar_url", + "star_count", "forks_count", "last_activity_at", "namespace" + ], + "additionalProperties": false +} diff --git a/spec/fixtures/api/schemas/public_api/v4/projects.json b/spec/fixtures/api/schemas/public_api/v4/projects.json index f0f8ede4c56..20f26a7f805 100644 --- a/spec/fixtures/api/schemas/public_api/v4/projects.json +++ b/spec/fixtures/api/schemas/public_api/v4/projects.json @@ -1,48 +1,4 @@ { "type": "array", - "items": { - "type": "object", - "properties" : { - "id": { "type": "integer" }, - "name": { "type": "string" }, - "name_with_namespace": { "type": "string" }, - "description": { "type": ["string", "null"] }, - "path": { "type": "string" }, - "path_with_namespace": { "type": "string" }, - "created_at": { "type": "string", "format": "date-time" }, - "default_branch": { "type": ["string", "null"] }, - "tag_list": { - "type": "array", - "items": { - "type": "string" - } - }, - "ssh_url_to_repo": { "type": "string" }, - "http_url_to_repo": { "type": "string" }, - "web_url": { "type": "string" }, - "readme_url": { "type": ["string", "null"] }, - "avatar_url": { "type": ["string", "null"] }, - "star_count": { "type": "integer" }, - "forks_count": { "type": "integer" }, - "last_activity_at": { "type": "string", "format": "date-time" }, - "namespace": { - "type": "object", - "properties" : { - "id": { "type": "integer" }, - "name": { "type": "string" }, - "path": { "type": "string" }, - "kind": { "type": "string" }, - "full_path": { "type": "string" }, - "parent_id": { "type": ["integer", "null"] } - } - } - }, - "required": [ - "id", "name", "name_with_namespace", "description", "path", - "path_with_namespace", "created_at", "default_branch", "tag_list", - "ssh_url_to_repo", "http_url_to_repo", "web_url", "avatar_url", - "star_count", "last_activity_at", "namespace" - ], - "additionalProperties": false - } + "items": { "$ref": "project.json" } } diff --git a/spec/fixtures/bulk_imports/labels.ndjson.gz b/spec/fixtures/bulk_imports/labels.ndjson.gz Binary files differnew file mode 100644 index 00000000000..6bb10a53346 --- /dev/null +++ b/spec/fixtures/bulk_imports/labels.ndjson.gz diff --git a/spec/frontend/error_tracking_settings/components/error_tracking_form_spec.js b/spec/frontend/error_tracking_settings/components/error_tracking_form_spec.js index f02a261f323..2e8a42dbfe6 100644 --- a/spec/frontend/error_tracking_settings/components/error_tracking_form_spec.js +++ b/spec/frontend/error_tracking_settings/components/error_tracking_form_spec.js @@ -33,11 +33,12 @@ describe('error tracking settings form', () => { describe('an empty form', () => { it('is rendered', () => { - expect(wrapper.findAll(GlFormInput).length).toBe(2); - expect(wrapper.find(GlFormInput).attributes('id')).toBe('error-tracking-api-host'); - expect(wrapper.findAll(GlFormInput).at(1).attributes('id')).toBe('error-tracking-token'); - - expect(wrapper.findAll(GlButton).exists()).toBe(true); + expect(wrapper.findAllComponents(GlFormInput).length).toBe(2); + expect(wrapper.findComponent(GlFormInput).attributes('id')).toBe('error-tracking-api-host'); + expect(wrapper.findAllComponents(GlFormInput).at(1).attributes('id')).toBe( + 'error-tracking-token', + ); + expect(wrapper.findAllComponents(GlButton).exists()).toBe(true); }); it('is rendered with labels and placeholders', () => { @@ -51,7 +52,7 @@ describe('error tracking settings form', () => { ); expect(pageText).not.toContain('Connection failed. Check Auth Token and try again.'); - expect(wrapper.findAll(GlFormInput).at(0).attributes('placeholder')).toContain( + expect(wrapper.findAllComponents(GlFormInput).at(0).attributes('placeholder')).toContain( 'https://mysentryserver.com', ); }); @@ -63,7 +64,7 @@ describe('error tracking settings form', () => { }); it('shows loading spinner', () => { - const buttonEl = wrapper.find(GlButton); + const buttonEl = wrapper.findComponent(GlButton); expect(buttonEl.props('loading')).toBe(true); expect(buttonEl.text()).toBe('Connecting'); diff --git a/spec/frontend/import_entities/import_groups/components/import_table_row_spec.js b/spec/frontend/import_entities/import_groups/components/import_table_row_spec.js index 7a83136e785..3080d7ebdb8 100644 --- a/spec/frontend/import_entities/import_groups/components/import_table_row_spec.js +++ b/spec/frontend/import_entities/import_groups/components/import_table_row_spec.js @@ -19,7 +19,7 @@ const getFakeGroup = (status) => ({ new_name: 'group1', }, id: 1, - status, + progress: { status }, }); const EXISTING_GROUP_TARGET_NAMESPACE = 'existing-group'; diff --git a/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js b/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js index 1feff861c1e..0d0679c74e5 100644 --- a/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js +++ b/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js @@ -9,9 +9,12 @@ import { createResolvers, } from '~/import_entities/import_groups/graphql/client_factory'; import importGroupMutation from '~/import_entities/import_groups/graphql/mutations/import_group.mutation.graphql'; +import setImportProgressMutation from '~/import_entities/import_groups/graphql/mutations/set_import_progress.mutation.graphql'; import setNewNameMutation from '~/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql'; import setTargetNamespaceMutation from '~/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql'; +import updateImportStatusMutation from '~/import_entities/import_groups/graphql/mutations/update_import_status.mutation.graphql'; import availableNamespacesQuery from '~/import_entities/import_groups/graphql/queries/available_namespaces.query.graphql'; +import bulkImportSourceGroupQuery from '~/import_entities/import_groups/graphql/queries/bulk_import_source_group.query.graphql'; import bulkImportSourceGroupsQuery from '~/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql'; import { StatusPoller } from '~/import_entities/import_groups/graphql/services/status_poller'; @@ -78,6 +81,31 @@ describe('Bulk import resolvers', () => { }); }); + describe('bulkImportSourceGroup', () => { + beforeEach(async () => { + axiosMockAdapter.onGet(FAKE_ENDPOINTS.status).reply(httpStatus.OK, statusEndpointFixture); + axiosMockAdapter + .onGet(FAKE_ENDPOINTS.availableNamespaces) + .reply(httpStatus.OK, availableNamespacesFixture); + + return client.query({ + query: bulkImportSourceGroupsQuery, + }); + }); + + it('returns group', async () => { + const { id } = statusEndpointFixture.importable_data[0]; + const { + data: { bulkImportSourceGroup: group }, + } = await client.query({ + query: bulkImportSourceGroupQuery, + variables: { id: id.toString() }, + }); + + expect(group).toMatchObject(statusEndpointFixture.importable_data[0]); + }); + }); + describe('bulkImportSourceGroups', () => { let results; @@ -89,8 +117,12 @@ describe('Bulk import resolvers', () => { }); it('respects cached import state when provided by group manager', async () => { + const FAKE_JOB_ID = '1'; const FAKE_STATUS = 'DEMO_STATUS'; - const FAKE_IMPORT_TARGET = {}; + const FAKE_IMPORT_TARGET = { + new_name: 'test-name', + target_namespace: 'test-namespace', + }; const TARGET_INDEX = 0; const clientWithMockedManager = createClient({ @@ -98,8 +130,11 @@ describe('Bulk import resolvers', () => { getImportStateFromStorageByGroupId(groupId) { if (groupId === statusEndpointFixture.importable_data[TARGET_INDEX].id) { return { - status: FAKE_STATUS, - importTarget: FAKE_IMPORT_TARGET, + jobId: FAKE_JOB_ID, + importState: { + status: FAKE_STATUS, + importTarget: FAKE_IMPORT_TARGET, + }, }; } @@ -113,8 +148,8 @@ describe('Bulk import resolvers', () => { }); const clientResults = clientResponse.data.bulkImportSourceGroups.nodes; - expect(clientResults[TARGET_INDEX].import_target).toBe(FAKE_IMPORT_TARGET); - expect(clientResults[TARGET_INDEX].status).toBe(FAKE_STATUS); + expect(clientResults[TARGET_INDEX].import_target).toStrictEqual(FAKE_IMPORT_TARGET); + expect(clientResults[TARGET_INDEX].progress.status).toBe(FAKE_STATUS); }); it('populates each result instance with empty import_target when there are no available namespaces', async () => { @@ -143,8 +178,8 @@ describe('Bulk import resolvers', () => { ).toBe(true); }); - it('populates each result instance with status field default to none', () => { - expect(results.every((r) => r.status === STATUSES.NONE)).toBe(true); + it('populates each result instance with status default to none', () => { + expect(results.every((r) => r.progress.status === STATUSES.NONE)).toBe(true); }); it('populates each result instance with import_target defaulted to first available namespace', () => { @@ -183,7 +218,6 @@ describe('Bulk import resolvers', () => { }); describe('mutations', () => { - let results; const GROUP_ID = 1; beforeEach(() => { @@ -195,7 +229,10 @@ describe('Bulk import resolvers', () => { { __typename: clientTypenames.BulkImportSourceGroup, id: GROUP_ID, - status: STATUSES.NONE, + progress: { + id: `test-${GROUP_ID}`, + status: STATUSES.NONE, + }, web_url: 'https://fake.host/1', full_path: 'fake_group_1', full_name: 'fake_name_1', @@ -214,35 +251,42 @@ describe('Bulk import resolvers', () => { }, }, }); - - client - .watchQuery({ - query: bulkImportSourceGroupsQuery, - fetchPolicy: 'cache-only', - }) - .subscribe(({ data }) => { - results = data.bulkImportSourceGroups.nodes; - }); }); it('setTargetNamespaces updates group target namespace', async () => { const NEW_TARGET_NAMESPACE = 'target'; - await client.mutate({ + const { + data: { + setTargetNamespace: { + id: idInResponse, + import_target: { target_namespace: namespaceInResponse }, + }, + }, + } = await client.mutate({ mutation: setTargetNamespaceMutation, variables: { sourceGroupId: GROUP_ID, targetNamespace: NEW_TARGET_NAMESPACE }, }); - expect(results[0].import_target.target_namespace).toBe(NEW_TARGET_NAMESPACE); + expect(idInResponse).toBe(GROUP_ID); + expect(namespaceInResponse).toBe(NEW_TARGET_NAMESPACE); }); it('setNewName updates group target name', async () => { const NEW_NAME = 'new'; - await client.mutate({ + const { + data: { + setNewName: { + id: idInResponse, + import_target: { new_name: nameInResponse }, + }, + }, + } = await client.mutate({ mutation: setNewNameMutation, variables: { sourceGroupId: GROUP_ID, newName: NEW_NAME }, }); - expect(results[0].import_target.new_name).toBe(NEW_NAME); + expect(idInResponse).toBe(GROUP_ID); + expect(nameInResponse).toBe(NEW_NAME); }); describe('importGroup', () => { @@ -261,33 +305,49 @@ describe('Bulk import resolvers', () => { query: bulkImportSourceGroupsQuery, }); - expect(intermediateResults[0].status).toBe(STATUSES.SCHEDULING); + expect(intermediateResults[0].progress.status).toBe(STATUSES.SCHEDULING); }); - it('sets import status to CREATED when request completes', async () => { - axiosMockAdapter.onPost(FAKE_ENDPOINTS.createBulkImport).reply(httpStatus.OK, { id: 1 }); - await client.mutate({ - mutation: importGroupMutation, - variables: { sourceGroupId: GROUP_ID }, + describe('when request completes', () => { + let results; + + beforeEach(() => { + client + .watchQuery({ + query: bulkImportSourceGroupsQuery, + fetchPolicy: 'cache-only', + }) + .subscribe(({ data }) => { + results = data.bulkImportSourceGroups.nodes; + }); }); - expect(results[0].status).toBe(STATUSES.CREATED); - }); - - it('resets status to NONE if request fails', async () => { - axiosMockAdapter - .onPost(FAKE_ENDPOINTS.createBulkImport) - .reply(httpStatus.INTERNAL_SERVER_ERROR); - - client - .mutate({ + it('sets import status to CREATED when request completes', async () => { + axiosMockAdapter.onPost(FAKE_ENDPOINTS.createBulkImport).reply(httpStatus.OK, { id: 1 }); + await client.mutate({ mutation: importGroupMutation, variables: { sourceGroupId: GROUP_ID }, - }) - .catch(() => {}); - await waitForPromises(); + }); + await waitForPromises(); + + expect(results[0].progress.status).toBe(STATUSES.CREATED); + }); + + it('resets status to NONE if request fails', async () => { + axiosMockAdapter + .onPost(FAKE_ENDPOINTS.createBulkImport) + .reply(httpStatus.INTERNAL_SERVER_ERROR); - expect(results[0].status).toBe(STATUSES.NONE); + client + .mutate({ + mutation: importGroupMutation, + variables: { sourceGroupId: GROUP_ID }, + }) + .catch(() => {}); + await waitForPromises(); + + expect(results[0].progress.status).toBe(STATUSES.NONE); + }); }); it('shows default error message when server error is not provided', async () => { @@ -324,5 +384,39 @@ describe('Bulk import resolvers', () => { expect(createFlash).toHaveBeenCalledWith({ message: CUSTOM_MESSAGE }); }); }); + + it('setImportProgress updates group progress', async () => { + const NEW_STATUS = 'dummy'; + const FAKE_JOB_ID = 5; + const { + data: { + setImportProgress: { progress }, + }, + } = await client.mutate({ + mutation: setImportProgressMutation, + variables: { sourceGroupId: GROUP_ID, status: NEW_STATUS, jobId: FAKE_JOB_ID }, + }); + + expect(progress).toMatchObject({ + id: FAKE_JOB_ID, + status: NEW_STATUS, + }); + }); + + it('updateImportStatus returns new status', async () => { + const NEW_STATUS = 'dummy'; + const FAKE_JOB_ID = 5; + const { + data: { updateImportStatus: statusInResponse }, + } = await client.mutate({ + mutation: updateImportStatusMutation, + variables: { id: FAKE_JOB_ID, status: NEW_STATUS }, + }); + + expect(statusInResponse).toMatchObject({ + id: FAKE_JOB_ID, + status: NEW_STATUS, + }); + }); }); }); diff --git a/spec/frontend/import_entities/import_groups/graphql/fixtures.js b/spec/frontend/import_entities/import_groups/graphql/fixtures.js index 62e9581bd2d..b046e04fa28 100644 --- a/spec/frontend/import_entities/import_groups/graphql/fixtures.js +++ b/spec/frontend/import_entities/import_groups/graphql/fixtures.js @@ -10,7 +10,10 @@ export const generateFakeEntry = ({ id, status, ...rest }) => ({ new_name: `group${id}`, }, id, - status, + progress: { + id: `test-${id}`, + status, + }, ...rest, }); diff --git a/spec/frontend/import_entities/import_groups/graphql/services/source_groups_manager_spec.js b/spec/frontend/import_entities/import_groups/graphql/services/source_groups_manager_spec.js index 5baa201906a..e9c214b8d3b 100644 --- a/spec/frontend/import_entities/import_groups/graphql/services/source_groups_manager_spec.js +++ b/spec/frontend/import_entities/import_groups/graphql/services/source_groups_manager_spec.js @@ -1,6 +1,3 @@ -import { defaultDataIdFromObject } from 'apollo-cache-inmemory'; -import { clientTypenames } from '~/import_entities/import_groups/graphql/client_factory'; -import ImportSourceGroupFragment from '~/import_entities/import_groups/graphql/fragments/bulk_import_source_group_item.fragment.graphql'; import { KEY, SourceGroupsManager, @@ -10,42 +7,29 @@ const FAKE_SOURCE_URL = 'http://demo.host'; describe('SourceGroupsManager', () => { let manager; - let client; let storage; - const getFakeGroup = () => ({ - __typename: clientTypenames.BulkImportSourceGroup, - id: 5, - }); - beforeEach(() => { - client = { - readFragment: jest.fn(), - writeFragment: jest.fn(), - }; storage = { getItem: jest.fn(), setItem: jest.fn(), }; - manager = new SourceGroupsManager({ client, storage, sourceUrl: FAKE_SOURCE_URL }); + manager = new SourceGroupsManager({ storage, sourceUrl: FAKE_SOURCE_URL }); }); describe('storage management', () => { const IMPORT_ID = 1; const IMPORT_TARGET = { destination_name: 'demo', destination_namespace: 'foo' }; const STATUS = 'FAKE_STATUS'; - const FAKE_GROUP = { id: 1, import_target: IMPORT_TARGET, status: STATUS }; + const FAKE_GROUP = { id: 1, importTarget: IMPORT_TARGET, status: STATUS }; it('loads state from storage on creation', () => { expect(storage.getItem).toHaveBeenCalledWith(KEY); }); - it('saves to storage when import is starting', () => { - manager.startImport({ - importId: IMPORT_ID, - group: FAKE_GROUP, - }); + it('saves to storage when saveImportState is called', () => { + manager.saveImportState(IMPORT_ID, FAKE_GROUP); const storedObject = JSON.parse(storage.setItem.mock.calls[0][1]); expect(Object.values(storedObject)[0]).toStrictEqual({ id: FAKE_GROUP.id, @@ -54,15 +38,12 @@ describe('SourceGroupsManager', () => { }); }); - it('saves to storage when import status is updated', () => { + it('updates storage when previous state is available', () => { const CHANGED_STATUS = 'changed'; - manager.startImport({ - importId: IMPORT_ID, - group: FAKE_GROUP, - }); + manager.saveImportState(IMPORT_ID, FAKE_GROUP); - manager.setImportStatusByImportId(IMPORT_ID, CHANGED_STATUS); + manager.saveImportState(IMPORT_ID, { status: CHANGED_STATUS }); const storedObject = JSON.parse(storage.setItem.mock.calls[1][1]); expect(Object.values(storedObject)[0]).toStrictEqual({ id: FAKE_GROUP.id, @@ -71,63 +52,4 @@ describe('SourceGroupsManager', () => { }); }); }); - - it('finds item by group id', () => { - const ID = 5; - - const FAKE_GROUP = getFakeGroup(); - client.readFragment.mockReturnValue(FAKE_GROUP); - const group = manager.findById(ID); - expect(group).toBe(FAKE_GROUP); - expect(client.readFragment).toHaveBeenCalledWith({ - fragment: ImportSourceGroupFragment, - id: defaultDataIdFromObject(getFakeGroup()), - }); - }); - - it('updates group with provided function', () => { - const UPDATED_GROUP = {}; - const fn = jest.fn().mockReturnValue(UPDATED_GROUP); - manager.update(getFakeGroup(), fn); - - expect(client.writeFragment).toHaveBeenCalledWith({ - fragment: ImportSourceGroupFragment, - id: defaultDataIdFromObject(getFakeGroup()), - data: UPDATED_GROUP, - }); - }); - - it('updates group by id with provided function', () => { - const UPDATED_GROUP = {}; - const fn = jest.fn().mockReturnValue(UPDATED_GROUP); - client.readFragment.mockReturnValue(getFakeGroup()); - manager.updateById(getFakeGroup().id, fn); - - expect(client.readFragment).toHaveBeenCalledWith({ - fragment: ImportSourceGroupFragment, - id: defaultDataIdFromObject(getFakeGroup()), - }); - - expect(client.writeFragment).toHaveBeenCalledWith({ - fragment: ImportSourceGroupFragment, - id: defaultDataIdFromObject(getFakeGroup()), - data: UPDATED_GROUP, - }); - }); - - it('sets import status when group is provided', () => { - client.readFragment.mockReturnValue(getFakeGroup()); - - const NEW_STATUS = 'NEW_STATUS'; - manager.setImportStatus(getFakeGroup(), NEW_STATUS); - - expect(client.writeFragment).toHaveBeenCalledWith({ - fragment: ImportSourceGroupFragment, - id: defaultDataIdFromObject(getFakeGroup()), - data: { - ...getFakeGroup(), - status: NEW_STATUS, - }, - }); - }); }); diff --git a/spec/frontend/import_entities/import_groups/graphql/services/status_poller_spec.js b/spec/frontend/import_entities/import_groups/graphql/services/status_poller_spec.js index 0d4809971ae..9c47647c430 100644 --- a/spec/frontend/import_entities/import_groups/graphql/services/status_poller_spec.js +++ b/spec/frontend/import_entities/import_groups/graphql/services/status_poller_spec.js @@ -21,17 +21,15 @@ const FAKE_POLL_PATH = '/fake/poll/path'; describe('Bulk import status poller', () => { let poller; let mockAdapter; - let groupManager; + let updateImportStatus; const getPollHistory = () => mockAdapter.history.get.filter((x) => x.url === FAKE_POLL_PATH); beforeEach(() => { mockAdapter = new MockAdapter(axios); mockAdapter.onGet(FAKE_POLL_PATH).reply(200, {}); - groupManager = { - setImportStatusByImportId: jest.fn(), - }; - poller = new StatusPoller({ groupManager, pollPath: FAKE_POLL_PATH }); + updateImportStatus = jest.fn(); + poller = new StatusPoller({ updateImportStatus, pollPath: FAKE_POLL_PATH }); }); it('creates poller with proper config', () => { @@ -96,9 +94,9 @@ describe('Bulk import status poller', () => { it('when success response arrives updates relevant group status', () => { const FAKE_ID = 5; const [[pollConfig]] = Poll.mock.calls; + const FAKE_RESPONSE = { id: FAKE_ID, status_name: STATUSES.FINISHED }; + pollConfig.successCallback({ data: [FAKE_RESPONSE] }); - pollConfig.successCallback({ data: [{ id: FAKE_ID, status_name: STATUSES.FINISHED }] }); - - expect(groupManager.setImportStatusByImportId).toHaveBeenCalledWith(FAKE_ID, STATUSES.FINISHED); + expect(updateImportStatus).toHaveBeenCalledWith(FAKE_RESPONSE); }); }); diff --git a/spec/frontend/sidebar/assignees_realtime_spec.js b/spec/frontend/sidebar/assignees_realtime_spec.js index 8b579d1f8f9..ecf33d6de37 100644 --- a/spec/frontend/sidebar/assignees_realtime_spec.js +++ b/spec/frontend/sidebar/assignees_realtime_spec.js @@ -4,7 +4,7 @@ import createMockApollo from 'helpers/mock_apollo_helper'; import AssigneesRealtime from '~/sidebar/components/assignees/assignees_realtime.vue'; import issuableAssigneesSubscription from '~/sidebar/queries/issuable_assignees.subscription.graphql'; import SidebarMediator from '~/sidebar/sidebar_mediator'; -import getIssueParticipantsQuery from '~/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql'; +import getIssueAssigneesQuery from '~/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql'; import Mock, { issuableQueryResponse, subscriptionNullResponse } from './mock_data'; const localVue = createLocalVue(); @@ -24,7 +24,7 @@ describe('Assignees Realtime', () => { subscriptionHandler = subscriptionInitialHandler, } = {}) => { fakeApollo = createMockApollo([ - [getIssueParticipantsQuery, issuableQueryHandler], + [getIssueAssigneesQuery, issuableQueryHandler], [issuableAssigneesSubscription, subscriptionHandler], ]); wrapper = shallowMount(AssigneesRealtime, { diff --git a/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js b/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js index 2973c25b936..3fbd863ab40 100644 --- a/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js +++ b/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js @@ -1,27 +1,20 @@ import { GlSearchBoxByType, GlDropdown } from '@gitlab/ui'; import { shallowMount, createLocalVue } from '@vue/test-utils'; -import { cloneDeep } from 'lodash'; import { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import createFlash from '~/flash'; -import searchUsersQuery from '~/graphql_shared/queries/users_search.query.graphql'; import { IssuableType } from '~/issue_show/constants'; import SidebarAssigneesRealtime from '~/sidebar/components/assignees/assignees_realtime.vue'; import IssuableAssignees from '~/sidebar/components/assignees/issuable_assignees.vue'; import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue'; import SidebarInviteMembers from '~/sidebar/components/assignees/sidebar_invite_members.vue'; import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue'; -import { ASSIGNEES_DEBOUNCE_DELAY } from '~/sidebar/constants'; -import MultiSelectDropdown from '~/vue_shared/components/sidebar/multiselect_dropdown.vue'; -import getIssueParticipantsQuery from '~/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql'; +import getIssueAssigneesQuery from '~/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql'; import updateIssueAssigneesMutation from '~/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql'; -import { - issuableQueryResponse, - searchQueryResponse, - updateIssueAssigneesMutationResponse, -} from '../../mock_data'; +import UserSelect from '~/vue_shared/components/user_select/user_select.vue'; +import { issuableQueryResponse, updateIssueAssigneesMutationResponse } from '../../mock_data'; jest.mock('~/flash'); @@ -50,31 +43,19 @@ describe('Sidebar assignees widget', () => { const findAssignees = () => wrapper.findComponent(IssuableAssignees); const findRealtimeAssignees = () => wrapper.findComponent(SidebarAssigneesRealtime); const findEditableItem = () => wrapper.findComponent(SidebarEditableItem); - const findDropdown = () => wrapper.findComponent(MultiSelectDropdown); const findInviteMembersLink = () => wrapper.findComponent(SidebarInviteMembers); - const findSearchField = () => wrapper.findComponent(GlSearchBoxByType); - - const findParticipantsLoading = () => wrapper.find('[data-testid="loading-participants"]'); - const findSelectedParticipants = () => wrapper.findAll('[data-testid="selected-participant"]'); - const findUnselectedParticipants = () => - wrapper.findAll('[data-testid="unselected-participant"]'); - const findCurrentUser = () => wrapper.findAll('[data-testid="current-user"]'); - const findUnassignLink = () => wrapper.find('[data-testid="unassign"]'); - const findEmptySearchResults = () => wrapper.find('[data-testid="empty-results"]'); + const findUserSelect = () => wrapper.findComponent(UserSelect); const expandDropdown = () => wrapper.vm.$refs.toggle.expand(); const createComponent = ({ - search = '', issuableQueryHandler = jest.fn().mockResolvedValue(issuableQueryResponse), - searchQueryHandler = jest.fn().mockResolvedValue(searchQueryResponse), updateIssueAssigneesMutationHandler = updateIssueAssigneesMutationSuccess, props = {}, provide = {}, } = {}) => { fakeApollo = createMockApollo([ - [getIssueParticipantsQuery, issuableQueryHandler], - [searchUsersQuery, searchQueryHandler], + [getIssueAssigneesQuery, issuableQueryHandler], [updateIssueAssigneesMutation, updateIssueAssigneesMutationHandler], ]); wrapper = shallowMount(SidebarAssigneesWidget, { @@ -82,15 +63,10 @@ describe('Sidebar assignees widget', () => { apolloProvider: fakeApollo, propsData: { iid: '1', + issuableId: 0, fullPath: '/mygroup/myProject', ...props, }, - data() { - return { - search, - selected: [], - }; - }, provide: { canUpdate: true, rootPath: '/', @@ -98,7 +74,7 @@ describe('Sidebar assignees widget', () => { }, stubs: { SidebarEditableItem, - MultiSelectDropdown, + UserSelect, GlSearchBoxByType, GlDropdown, }, @@ -148,19 +124,6 @@ describe('Sidebar assignees widget', () => { expect(findEditableItem().props('title')).toBe('Assignee'); }); - - describe('when expanded', () => { - it('renders a loading spinner if participants are loading', () => { - createComponent({ - props: { - initialAssignees, - }, - }); - expandDropdown(); - - expect(findParticipantsLoading().exists()).toBe(true); - }); - }); }); describe('without passed initial assignees', () => { @@ -198,7 +161,7 @@ describe('Sidebar assignees widget', () => { findAssignees().vm.$emit('assign-self'); expect(updateIssueAssigneesMutationSuccess).toHaveBeenCalledWith({ - assigneeUsernames: 'root', + assigneeUsernames: ['root'], fullPath: '/mygroup/myProject', iid: '1', }); @@ -220,7 +183,7 @@ describe('Sidebar assignees widget', () => { findAssignees().vm.$emit('assign-self'); expect(updateIssueAssigneesMutationSuccess).toHaveBeenCalledWith({ - assigneeUsernames: 'root', + assigneeUsernames: ['root'], fullPath: '/mygroup/myProject', iid: '1', }); @@ -245,18 +208,6 @@ describe('Sidebar assignees widget', () => { ]); }); - it('renders current user if they are not in participants or assignees', async () => { - gon.current_username = 'random'; - gon.current_user_fullname = 'Mr Random'; - gon.current_user_avatar_url = '/random'; - - createComponent(); - await waitForPromises(); - expandDropdown(); - - expect(findCurrentUser().exists()).toBe(true); - }); - describe('when expanded', () => { beforeEach(async () => { createComponent(); @@ -264,27 +215,18 @@ describe('Sidebar assignees widget', () => { expandDropdown(); }); - it('collapses the widget on multiselect dropdown toggle event', async () => { - findDropdown().vm.$emit('toggle'); + it('collapses the widget on user select toggle event', async () => { + findUserSelect().vm.$emit('toggle'); await nextTick(); - expect(findDropdown().isVisible()).toBe(false); - }); - - it('renders participants list with correct amount of selected and unselected', async () => { - expect(findSelectedParticipants()).toHaveLength(1); - expect(findUnselectedParticipants()).toHaveLength(2); - }); - - it('does not render current user if they are in participants', () => { - expect(findCurrentUser().exists()).toBe(false); + expect(findUserSelect().isVisible()).toBe(false); }); - it('unassigns all participants when clicking on `Unassign`', () => { - findUnassignLink().vm.$emit('click'); + it('calls an update mutation with correct variables on User Select input event', () => { + findUserSelect().vm.$emit('input', [{ username: 'root' }]); findEditableItem().vm.$emit('close'); expect(updateIssueAssigneesMutationSuccess).toHaveBeenCalledWith({ - assigneeUsernames: [], + assigneeUsernames: ['root'], fullPath: '/mygroup/myProject', iid: '1', }); @@ -293,68 +235,38 @@ describe('Sidebar assignees widget', () => { describe('when multiselect is disabled', () => { beforeEach(async () => { - createComponent({ props: { multipleAssignees: false } }); + createComponent({ props: { allowMultipleAssignees: false } }); await waitForPromises(); expandDropdown(); }); - it('adds a single assignee when clicking on unselected user', async () => { - findUnselectedParticipants().at(0).vm.$emit('click'); + it('closes a dropdown after User Select input event', async () => { + findUserSelect().vm.$emit('input', [{ username: 'root' }]); expect(updateIssueAssigneesMutationSuccess).toHaveBeenCalledWith({ assigneeUsernames: ['root'], fullPath: '/mygroup/myProject', iid: '1', }); - }); - it('removes an assignee when clicking on selected user', () => { - findSelectedParticipants().at(0).vm.$emit('click', new Event('click')); + await waitForPromises(); - expect(updateIssueAssigneesMutationSuccess).toHaveBeenCalledWith({ - assigneeUsernames: [], - fullPath: '/mygroup/myProject', - iid: '1', - }); + expect(findUserSelect().isVisible()).toBe(false); }); }); describe('when multiselect is enabled', () => { beforeEach(async () => { - createComponent({ props: { multipleAssignees: true } }); + createComponent({ props: { allowMultipleAssignees: true } }); await waitForPromises(); expandDropdown(); }); - it('adds a few assignees after clicking on unselected users and closing a dropdown', () => { - findUnselectedParticipants().at(0).vm.$emit('click'); - findUnselectedParticipants().at(1).vm.$emit('click'); - findEditableItem().vm.$emit('close'); - - expect(updateIssueAssigneesMutationSuccess).toHaveBeenCalledWith({ - assigneeUsernames: ['francina.skiles', 'root', 'johndoe'], - fullPath: '/mygroup/myProject', - iid: '1', - }); - }); - - it('removes an assignee when clicking on selected user and then closing dropdown', () => { - findSelectedParticipants().at(0).vm.$emit('click', new Event('click')); - - findEditableItem().vm.$emit('close'); - - expect(updateIssueAssigneesMutationSuccess).toHaveBeenCalledWith({ - assigneeUsernames: [], - fullPath: '/mygroup/myProject', - iid: '1', - }); - }); - it('does not call a mutation when clicking on participants until dropdown is closed', () => { - findUnselectedParticipants().at(0).vm.$emit('click'); - findSelectedParticipants().at(0).vm.$emit('click', new Event('click')); + findUserSelect().vm.$emit('input', [{ username: 'root' }]); expect(updateIssueAssigneesMutationSuccess).not.toHaveBeenCalled(); + expect(findUserSelect().isVisible()).toBe(true); }); }); @@ -363,7 +275,7 @@ describe('Sidebar assignees widget', () => { await waitForPromises(); expandDropdown(); - findUnassignLink().vm.$emit('click'); + findUserSelect().vm.$emit('input', []); findEditableItem().vm.$emit('close'); await waitForPromises(); @@ -372,95 +284,6 @@ describe('Sidebar assignees widget', () => { message: 'An error occurred while updating assignees.', }); }); - - describe('when searching', () => { - it('does not show loading spinner when debounce timer is still running', async () => { - createComponent({ search: 'roo' }); - await waitForPromises(); - expandDropdown(); - - expect(findParticipantsLoading().exists()).toBe(false); - }); - - it('shows loading spinner when searching for users', async () => { - createComponent({ search: 'roo' }); - await waitForPromises(); - expandDropdown(); - jest.advanceTimersByTime(ASSIGNEES_DEBOUNCE_DELAY); - await nextTick(); - - expect(findParticipantsLoading().exists()).toBe(true); - }); - - it('renders a list of found users and external participants matching search term', async () => { - const responseCopy = cloneDeep(issuableQueryResponse); - responseCopy.data.workspace.issuable.participants.nodes.push({ - id: 'gid://gitlab/User/5', - avatarUrl: '/someavatar', - name: 'Roodie', - username: 'roodie', - webUrl: '/roodie', - status: null, - }); - - const issuableQueryHandler = jest.fn().mockResolvedValue(responseCopy); - - createComponent({ issuableQueryHandler }); - await waitForPromises(); - expandDropdown(); - - findSearchField().vm.$emit('input', 'roo'); - await nextTick(); - - jest.advanceTimersByTime(ASSIGNEES_DEBOUNCE_DELAY); - await nextTick(); - await waitForPromises(); - - expect(findUnselectedParticipants()).toHaveLength(3); - }); - - it('renders a list of found users only if no external participants match search term', async () => { - createComponent({ search: 'roo' }); - await waitForPromises(); - expandDropdown(); - jest.advanceTimersByTime(250); - await nextTick(); - await waitForPromises(); - - expect(findUnselectedParticipants()).toHaveLength(2); - }); - - it('shows a message about no matches if search returned an empty list', async () => { - const responseCopy = cloneDeep(searchQueryResponse); - responseCopy.data.workspace.users.nodes = []; - - createComponent({ - search: 'roo', - searchQueryHandler: jest.fn().mockResolvedValue(responseCopy), - }); - await waitForPromises(); - expandDropdown(); - jest.advanceTimersByTime(ASSIGNEES_DEBOUNCE_DELAY); - await nextTick(); - await waitForPromises(); - - expect(findUnselectedParticipants()).toHaveLength(0); - expect(findEmptySearchResults().exists()).toBe(true); - }); - - it('shows an error if search query was rejected', async () => { - createComponent({ search: 'roo', searchQueryHandler: mockError }); - await waitForPromises(); - expandDropdown(); - jest.advanceTimersByTime(250); - await nextTick(); - await waitForPromises(); - - expect(createFlash).toHaveBeenCalledWith({ - message: 'An error occurred while searching users.', - }); - }); - }); }); describe('when user is not signed in', () => { @@ -469,11 +292,6 @@ describe('Sidebar assignees widget', () => { createComponent(); }); - it('does not show current user in the dropdown', () => { - expandDropdown(); - expect(findCurrentUser().exists()).toBe(false); - }); - it('passes signedIn prop as false to IssuableAssignees', () => { expect(findAssignees().props('signedIn')).toBe(false); }); @@ -487,9 +305,6 @@ describe('Sidebar assignees widget', () => { it('when realtime feature flag is enabled', async () => { createComponent({ - props: { - issuableId: 1, - }, provide: { glFeatures: { realTimeIssueSidebar: true, @@ -510,17 +325,17 @@ describe('Sidebar assignees widget', () => { expect(findEditableItem().props('isDirty')).toBe(false); }); - it('passes truthy `isDirty` prop if selected users list was changed', async () => { + it('passes truthy `isDirty` prop after User Select component emitted an input event', async () => { expandDropdown(); expect(findEditableItem().props('isDirty')).toBe(false); - findUnselectedParticipants().at(0).vm.$emit('click'); + findUserSelect().vm.$emit('input', []); await nextTick(); expect(findEditableItem().props('isDirty')).toBe(true); }); it('passes falsy `isDirty` prop after dropdown is closed', async () => { expandDropdown(); - findUnselectedParticipants().at(0).vm.$emit('click'); + findUserSelect().vm.$emit('input', []); findEditableItem().vm.$emit('close'); await waitForPromises(); expect(findEditableItem().props('isDirty')).toBe(false); diff --git a/spec/frontend/sidebar/mock_data.js b/spec/frontend/sidebar/mock_data.js index 787b36a6c56..38fce53e398 100644 --- a/spec/frontend/sidebar/mock_data.js +++ b/spec/frontend/sidebar/mock_data.js @@ -283,38 +283,6 @@ export const issuableQueryResponse = { __typename: 'Issue', id: 'gid://gitlab/Issue/1', iid: '1', - participants: { - nodes: [ - { - id: 'gid://gitlab/User/1', - avatarUrl: - 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', - name: 'Administrator', - username: 'root', - webUrl: '/root', - status: null, - }, - { - id: 'gid://gitlab/User/2', - avatarUrl: - 'https://www.gravatar.com/avatar/a95e5b71488f4b9d69ce5ff58bfd28d6?s=80\u0026d=identicon', - name: 'Jacki Kub', - username: 'francina.skiles', - webUrl: '/franc', - status: { - availability: 'BUSY', - }, - }, - { - id: 'gid://gitlab/User/3', - avatarUrl: '/avatar', - name: 'John Doe', - username: 'johndoe', - webUrl: '/john', - status: null, - }, - ], - }, assignees: { nodes: [ { @@ -386,10 +354,107 @@ export const updateIssueAssigneesMutationResponse = { ], __typename: 'UserConnection', }, + __typename: 'Issue', + }, + }, + }, +}; + +export const subscriptionNullResponse = { + data: { + issuableAssigneesUpdated: null, + }, +}; + +export const searchResponse = { + data: { + workspace: { + __typename: 'Project', + users: { + nodes: [ + { + user: { + id: '1', + avatarUrl: '/avatar', + name: 'root', + username: 'root', + webUrl: 'root', + status: null, + }, + }, + { + user: { + id: '2', + avatarUrl: '/avatar2', + name: 'rookie', + username: 'rookie', + webUrl: 'rookie', + status: null, + }, + }, + ], + }, + }, + }, +}; + +export const projectMembersResponse = { + data: { + workspace: { + __typename: 'Project', + users: { + nodes: [ + { + user: { + id: 'gid://gitlab/User/1', + avatarUrl: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', + name: 'Administrator', + username: 'root', + webUrl: '/root', + status: null, + }, + }, + { + user: { + id: '2', + avatarUrl: '/avatar2', + name: 'rookie', + username: 'rookie', + webUrl: 'rookie', + status: null, + }, + }, + { + user: { + id: 'gid://gitlab/User/2', + avatarUrl: + 'https://www.gravatar.com/avatar/a95e5b71488f4b9d69ce5ff58bfd28d6?s=80\u0026d=identicon', + name: 'Jacki Kub', + username: 'francina.skiles', + webUrl: '/franc', + status: { + availability: 'BUSY', + }, + }, + }, + ], + }, + }, + }, +}; + +export const participantsQueryResponse = { + data: { + workspace: { + __typename: 'Project', + issuable: { + __typename: 'Issue', + id: 'gid://gitlab/Issue/1', + iid: '1', participants: { nodes: [ { - __typename: 'User', id: 'gid://gitlab/User/1', avatarUrl: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', @@ -399,28 +464,29 @@ export const updateIssueAssigneesMutationResponse = { status: null, }, { - __typename: 'User', id: 'gid://gitlab/User/2', avatarUrl: 'https://www.gravatar.com/avatar/a95e5b71488f4b9d69ce5ff58bfd28d6?s=80\u0026d=identicon', name: 'Jacki Kub', username: 'francina.skiles', webUrl: '/franc', + status: { + availability: 'BUSY', + }, + }, + { + id: 'gid://gitlab/User/3', + avatarUrl: '/avatar', + name: 'John Doe', + username: 'rollie', + webUrl: '/john', status: null, }, ], - __typename: 'UserConnection', }, - __typename: 'Issue', }, }, }, }; -export const subscriptionNullResponse = { - data: { - issuableAssigneesUpdated: null, - }, -}; - export default mockData; diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_merging_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_merging_spec.js index 37d742e92f1..b6c16958993 100644 --- a/spec/frontend/vue_mr_widget/components/states/mr_widget_merging_spec.js +++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_merging_spec.js @@ -31,7 +31,7 @@ describe('MRWidgetMerging', () => { .trim() .replace(/\s\s+/g, ' ') .replace(/[\r\n]+/g, ' '), - ).toContain('Merging! Drum roll, please…'); + ).toContain('Merging!'); }); it('renders branch information', () => { diff --git a/spec/frontend/vue_shared/components/user_select_spec.js b/spec/frontend/vue_shared/components/user_select_spec.js new file mode 100644 index 00000000000..4258d415402 --- /dev/null +++ b/spec/frontend/vue_shared/components/user_select_spec.js @@ -0,0 +1,267 @@ +import { GlSearchBoxByType, GlDropdown } from '@gitlab/ui'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import { cloneDeep } from 'lodash'; +import { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import searchUsersQuery from '~/graphql_shared/queries/users_search.query.graphql'; +import { ASSIGNEES_DEBOUNCE_DELAY } from '~/sidebar/constants'; +import getIssueParticipantsQuery from '~/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql'; +import UserSelect from '~/vue_shared/components/user_select/user_select.vue'; +import { + searchResponse, + projectMembersResponse, + participantsQueryResponse, +} from '../../sidebar/mock_data'; + +const assignee = { + id: 'gid://gitlab/User/4', + avatarUrl: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', + name: 'Developer', + username: 'dev', + webUrl: '/dev', + status: null, +}; + +const mockError = jest.fn().mockRejectedValue('Error!'); + +const waitForSearch = async () => { + jest.advanceTimersByTime(ASSIGNEES_DEBOUNCE_DELAY); + await nextTick(); + await waitForPromises(); +}; + +const localVue = createLocalVue(); +localVue.use(VueApollo); + +describe('User select dropdown', () => { + let wrapper; + let fakeApollo; + + const findSearchField = () => wrapper.findComponent(GlSearchBoxByType); + const findParticipantsLoading = () => wrapper.find('[data-testid="loading-participants"]'); + const findSelectedParticipants = () => wrapper.findAll('[data-testid="selected-participant"]'); + const findUnselectedParticipants = () => + wrapper.findAll('[data-testid="unselected-participant"]'); + const findCurrentUser = () => wrapper.findAll('[data-testid="current-user"]'); + const findUnassignLink = () => wrapper.find('[data-testid="unassign"]'); + const findEmptySearchResults = () => wrapper.find('[data-testid="empty-results"]'); + + const createComponent = ({ + props = {}, + searchQueryHandler = jest.fn().mockResolvedValue(projectMembersResponse), + participantsQueryHandler = jest.fn().mockResolvedValue(participantsQueryResponse), + } = {}) => { + fakeApollo = createMockApollo([ + [searchUsersQuery, searchQueryHandler], + [getIssueParticipantsQuery, participantsQueryHandler], + ]); + wrapper = shallowMount(UserSelect, { + localVue, + apolloProvider: fakeApollo, + propsData: { + headerText: 'test', + text: 'test-text', + fullPath: '/project', + iid: '1', + value: [], + currentUser: { + username: 'random', + name: 'Mr. Random', + }, + allowMultipleAssignees: false, + ...props, + }, + stubs: { + GlDropdown, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + fakeApollo = null; + }); + + it('renders a loading spinner if participants are loading', () => { + createComponent(); + + expect(findParticipantsLoading().exists()).toBe(true); + }); + + it('emits an `error` event if participants query was rejected', async () => { + createComponent({ participantsQueryHandler: mockError }); + await waitForPromises(); + + expect(wrapper.emitted('error')).toBeTruthy(); + }); + + it('emits an `error` event if search query was rejected', async () => { + createComponent({ searchQueryHandler: mockError }); + await waitForSearch(); + + expect(wrapper.emitted('error')).toBeTruthy(); + }); + + it('renders current user if they are not in participants or assignees', async () => { + createComponent(); + await waitForPromises(); + + expect(findCurrentUser().exists()).toBe(true); + }); + + it('displays correct amount of selected users', async () => { + createComponent({ + props: { + value: [assignee], + }, + }); + await waitForPromises(); + + expect(findSelectedParticipants()).toHaveLength(1); + }); + + describe('when search is empty', () => { + it('renders a merged list of participants and project members', async () => { + createComponent(); + await waitForPromises(); + expect(findUnselectedParticipants()).toHaveLength(3); + }); + + it('renders `Unassigned` link with the checkmark when there are no selected users', async () => { + createComponent(); + await waitForPromises(); + expect(findUnassignLink().props('isChecked')).toBe(true); + }); + + it('renders `Unassigned` link without the checkmark when there are selected users', async () => { + createComponent({ + props: { + value: [assignee], + }, + }); + await waitForPromises(); + expect(findUnassignLink().props('isChecked')).toBe(false); + }); + + it('emits an input event with empty array after clicking on `Unassigned`', async () => { + createComponent({ + props: { + value: [assignee], + }, + }); + await waitForPromises(); + findUnassignLink().vm.$emit('click'); + + expect(wrapper.emitted('input')).toEqual([[[]]]); + }); + + it('emits an empty array after unselecting the only selected assignee', async () => { + createComponent({ + props: { + value: [assignee], + }, + }); + await waitForPromises(); + + findSelectedParticipants().at(0).vm.$emit('click', new Event('click')); + expect(wrapper.emitted('input')).toEqual([[[]]]); + }); + + it('allows only one user to be selected if `allowMultipleAssignees` is false', async () => { + createComponent({ + props: { + value: [assignee], + }, + }); + await waitForPromises(); + + findUnselectedParticipants().at(0).vm.$emit('click'); + expect(wrapper.emitted('input')).toEqual([ + [ + [ + { + avatarUrl: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + id: 'gid://gitlab/User/1', + name: 'Administrator', + status: null, + username: 'root', + webUrl: '/root', + }, + ], + ], + ]); + }); + + it('adds user to selected if `allowMultipleAssignees` is true', async () => { + createComponent({ + props: { + value: [assignee], + allowMultipleAssignees: true, + }, + }); + await waitForPromises(); + + findUnselectedParticipants().at(0).vm.$emit('click'); + expect(wrapper.emitted('input')[0][0]).toHaveLength(2); + }); + }); + + describe('when searching', () => { + it('does not show loading spinner when debounce timer is still running', async () => { + createComponent(); + await waitForPromises(); + findSearchField().vm.$emit('input', 'roo'); + + expect(findParticipantsLoading().exists()).toBe(false); + }); + + it('shows loading spinner when searching for users', async () => { + createComponent(); + await waitForPromises(); + findSearchField().vm.$emit('input', 'roo'); + jest.advanceTimersByTime(ASSIGNEES_DEBOUNCE_DELAY); + await nextTick(); + + expect(findParticipantsLoading().exists()).toBe(true); + }); + + it('renders a list of found users and external participants matching search term', async () => { + createComponent({ searchQueryHandler: jest.fn().mockResolvedValue(searchResponse) }); + await waitForPromises(); + + findSearchField().vm.$emit('input', 'ro'); + await waitForSearch(); + + expect(findUnselectedParticipants()).toHaveLength(3); + }); + + it('renders a list of found users only if no external participants match search term', async () => { + createComponent({ searchQueryHandler: jest.fn().mockResolvedValue(searchResponse) }); + await waitForPromises(); + + findSearchField().vm.$emit('input', 'roo'); + await waitForSearch(); + + expect(findUnselectedParticipants()).toHaveLength(2); + }); + + it('shows a message about no matches if search returned an empty list', async () => { + const responseCopy = cloneDeep(searchResponse); + responseCopy.data.workspace.users.nodes = []; + + createComponent({ + searchQueryHandler: jest.fn().mockResolvedValue(responseCopy), + }); + await waitForPromises(); + findSearchField().vm.$emit('input', 'tango'); + await waitForSearch(); + + expect(findUnselectedParticipants()).toHaveLength(0); + expect(findEmptySearchResults().exists()).toBe(true); + }); + }); +}); diff --git a/spec/helpers/learn_gitlab_helper_spec.rb b/spec/helpers/learn_gitlab_helper_spec.rb index 82c8e4ba596..796542aa2b5 100644 --- a/spec/helpers/learn_gitlab_helper_spec.rb +++ b/spec/helpers/learn_gitlab_helper_spec.rb @@ -7,14 +7,14 @@ RSpec.describe LearnGitlabHelper do include Devise::Test::ControllerHelpers let_it_be(:user) { create(:user) } - let_it_be(:project) { create(:project, name: LearnGitlab::PROJECT_NAME, namespace: user.namespace) } + let_it_be(:project) { create(:project, name: LearnGitlab::Project::PROJECT_NAME, namespace: user.namespace) } let_it_be(:namespace) { project.namespace } before do project.add_developer(user) allow(helper).to receive(:user).and_return(user) - allow_next_instance_of(LearnGitlab) do |learn_gitlab| + allow_next_instance_of(LearnGitlab::Project) do |learn_gitlab| allow(learn_gitlab).to receive(:project).and_return(project) end @@ -41,12 +41,12 @@ RSpec.describe LearnGitlabHelper do it 'sets correct path and completion status' do expect(onboarding_actions_data[:git_write]).to eq({ - url: project_issue_url(project, LearnGitlabHelper::ACTION_ISSUE_IDS[:git_write]), + url: project_issue_url(project, LearnGitlab::Onboarding::ACTION_ISSUE_IDS[:git_write]), completed: true, svg: helper.image_path("learn_gitlab/git_write.svg") }) expect(onboarding_actions_data[:pipeline_created]).to eq({ - url: project_issue_url(project, LearnGitlabHelper::ACTION_ISSUE_IDS[:pipeline_created]), + url: project_issue_url(project, LearnGitlab::Onboarding::ACTION_ISSUE_IDS[:pipeline_created]), completed: false, svg: helper.image_path("learn_gitlab/pipeline_created.svg") }) @@ -75,7 +75,7 @@ RSpec.describe LearnGitlabHelper do before do stub_experiment_for_subject(learn_gitlab_a: experiment_a, learn_gitlab_b: experiment_b) allow(OnboardingProgress).to receive(:onboarding?).with(project.namespace).and_return(onboarding) - allow_next(LearnGitlab, user).to receive(:available?).and_return(learn_gitlab_available) + allow_next(LearnGitlab::Project, user).to receive(:available?).and_return(learn_gitlab_available) end context 'when signed in' do diff --git a/spec/lib/gitlab/experimentation/controller_concern_spec.rb b/spec/lib/gitlab/experimentation/controller_concern_spec.rb index 3678aeb18b0..5419a01ea3e 100644 --- a/spec/lib/gitlab/experimentation/controller_concern_spec.rb +++ b/spec/lib/gitlab/experimentation/controller_concern_spec.rb @@ -19,12 +19,15 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do } ) + allow(Gitlab).to receive(:dev_env_or_com?).and_return(is_gitlab_com) + Feature.enable_percentage_of_time(:backwards_compatible_test_experiment_experiment_percentage, enabled_percentage) Feature.enable_percentage_of_time(:test_experiment_experiment_percentage, enabled_percentage) end let(:enabled_percentage) { 10 } let(:rollout_strategy) { nil } + let(:is_gitlab_com) { true } controller(ApplicationController) do include Gitlab::Experimentation::ControllerConcern @@ -37,17 +40,17 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do describe '#set_experimentation_subject_id_cookie' do let(:do_not_track) { nil } let(:cookie) { cookies.permanent.signed[:experimentation_subject_id] } + let(:cookie_value) { nil } before do request.headers['DNT'] = do_not_track if do_not_track.present? + request.cookies[:experimentation_subject_id] = cookie_value if cookie_value get :index end context 'cookie is present' do - before do - cookies[:experimentation_subject_id] = 'test' - end + let(:cookie_value) { 'test' } it 'does not change the cookie' do expect(cookies[:experimentation_subject_id]).to eq 'test' @@ -75,6 +78,24 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do end end end + + context 'when not on gitlab.com' do + let(:is_gitlab_com) { false } + + context 'when cookie was set' do + let(:cookie_value) { 'test' } + + it 'cookie gets deleted' do + expect(cookie).not_to be_present + end + end + + context 'when no cookie was set before' do + it 'does nothing' do + expect(cookie).not_to be_present + end + end + end end describe '#push_frontend_experiment' do diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 04bd754e664..340556c0dc4 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -746,3 +746,5 @@ issuable_sla: - issue push_rule: - group +bulk_import_export: + - group diff --git a/spec/lib/learn_gitlab/onboarding_spec.rb b/spec/lib/learn_gitlab/onboarding_spec.rb new file mode 100644 index 00000000000..6b4be65f3b2 --- /dev/null +++ b/spec/lib/learn_gitlab/onboarding_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe LearnGitlab::Onboarding do + describe '#completed_percentage' do + let(:completed_actions) { {} } + let(:onboarding_progress) { build(:onboarding_progress, namespace: namespace, **completed_actions) } + let(:namespace) { build(:namespace) } + + let_it_be(:tracked_action_columns) do + tracked_actions = described_class::ACTION_ISSUE_IDS.keys + described_class::ACTION_DOC_URLS.keys + tracked_actions.map { |key| OnboardingProgress.column_name(key) } + end + + before do + expect(OnboardingProgress).to receive(:find_by).with(namespace: namespace).and_return(onboarding_progress) + end + + subject { described_class.new(namespace).completed_percentage } + + context 'when no onboarding_progress exists' do + let(:onboarding_progress) { nil } + + it { is_expected.to eq(0) } + end + + context 'when no action has been completed' do + it { is_expected.to eq(0) } + end + + context 'when one action has been completed' do + let(:completed_actions) { Hash[tracked_action_columns.first, Time.current] } + + it { is_expected.to eq(11) } + end + + context 'when all tracked actions have been completed' do + let(:completed_actions) do + tracked_action_columns.to_h { |action| [action, Time.current] } + end + + it { is_expected.to eq(100) } + end + end +end diff --git a/spec/lib/learn_gitlab_spec.rb b/spec/lib/learn_gitlab/project_spec.rb index abfd82999c3..523703761bf 100644 --- a/spec/lib/learn_gitlab_spec.rb +++ b/spec/lib/learn_gitlab/project_spec.rb @@ -2,11 +2,11 @@ require 'spec_helper' -RSpec.describe LearnGitlab do +RSpec.describe LearnGitlab::Project do let_it_be(:current_user) { create(:user) } - let_it_be(:learn_gitlab_project) { create(:project, name: LearnGitlab::PROJECT_NAME) } - let_it_be(:learn_gitlab_board) { create(:board, project: learn_gitlab_project, name: LearnGitlab::BOARD_NAME) } - let_it_be(:learn_gitlab_label) { create(:label, project: learn_gitlab_project, name: LearnGitlab::LABEL_NAME) } + let_it_be(:learn_gitlab_project) { create(:project, name: LearnGitlab::Project::PROJECT_NAME) } + let_it_be(:learn_gitlab_board) { create(:board, project: learn_gitlab_project, name: LearnGitlab::Project::BOARD_NAME) } + let_it_be(:learn_gitlab_label) { create(:label, project: learn_gitlab_project, name: LearnGitlab::Project::LABEL_NAME) } before do learn_gitlab_project.add_developer(current_user) diff --git a/spec/lib/sidebars/projects/menus/learn_gitlab_menu_spec.rb b/spec/lib/sidebars/projects/menus/learn_gitlab_menu_spec.rb index 3dfb4c6e9e8..6bcc62bd973 100644 --- a/spec/lib/sidebars/projects/menus/learn_gitlab_menu_spec.rb +++ b/spec/lib/sidebars/projects/menus/learn_gitlab_menu_spec.rb @@ -28,4 +28,32 @@ RSpec.describe Sidebars::Projects::Menus::LearnGitlabMenu do end end end + + describe '#has_pill?' do + context 'when learn gitlab experiment is enabled' do + it 'returns true' do + expect(subject.has_pill?).to eq true + end + end + + context 'when learn gitlab experiment is disabled' do + let(:experiment_enabled) { false } + + it 'returns false' do + expect(subject.has_pill?).to eq false + end + end + end + + describe '#pill_count' do + before do + expect_next_instance_of(LearnGitlab::Onboarding) do |onboarding| + expect(onboarding).to receive(:completed_percentage).and_return(20) + end + end + + it 'returns pill count' do + expect(subject.pill_count).to eq '20%' + end + end end diff --git a/spec/models/bulk_imports/export_spec.rb b/spec/models/bulk_imports/export_spec.rb new file mode 100644 index 00000000000..26d25e6901e --- /dev/null +++ b/spec/models/bulk_imports/export_spec.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::Export, type: :model do + describe 'associations' do + it { is_expected.to belong_to(:group) } + it { is_expected.to belong_to(:project) } + it { is_expected.to have_one(:upload) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:relation) } + it { is_expected.to validate_presence_of(:status) } + + context 'when not associated with a group or project' do + it 'is invalid' do + export = build(:bulk_import_export, group: nil, project: nil) + + expect(export).not_to be_valid + end + end + + context 'when associated with a group' do + it 'is valid' do + export = build(:bulk_import_export, group: build(:group), project: nil) + + expect(export).to be_valid + end + end + + context 'when associated with a project' do + it 'is valid' do + export = build(:bulk_import_export, group: nil, project: build(:project)) + + expect(export).to be_valid + end + end + + context 'when relation is invalid' do + it 'is invalid' do + export = build(:bulk_import_export, relation: 'unsupported') + + expect(export).not_to be_valid + expect(export.errors).to include(:relation) + end + end + end + + describe '#exportable' do + context 'when associated with project' do + it 'returns project' do + export = create(:bulk_import_export, project: create(:project), group: nil) + + expect(export.exportable).to be_instance_of(Project) + end + end + + context 'when associated with group' do + it 'returns group' do + export = create(:bulk_import_export) + + expect(export.exportable).to be_instance_of(Group) + end + end + end + + describe '#config' do + context 'when associated with project' do + it 'returns project config' do + export = create(:bulk_import_export, project: create(:project), group: nil) + + expect(export.config).to be_instance_of(BulkImports::Exports::ProjectConfig) + end + end + + context 'when associated with group' do + it 'returns group config' do + export = create(:bulk_import_export) + + expect(export.config).to be_instance_of(BulkImports::Exports::GroupConfig) + end + end + end +end diff --git a/spec/models/bulk_imports/export_upload_spec.rb b/spec/models/bulk_imports/export_upload_spec.rb new file mode 100644 index 00000000000..641fa4a1b6c --- /dev/null +++ b/spec/models/bulk_imports/export_upload_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::ExportUpload do + subject { described_class.new(export: create(:bulk_import_export)) } + + describe 'associations' do + it { is_expected.to belong_to(:export) } + end + + it 'stores export file' do + method = 'export_file' + filename = 'labels.ndjson.gz' + + subject.public_send("#{method}=", fixture_file_upload("spec/fixtures/bulk_imports/#{filename}")) + subject.save! + + url = "/uploads/-/system/bulk_imports/export_upload/export_file/#{subject.id}/#{filename}" + + expect(subject.public_send(method).url).to eq(url) + end +end diff --git a/spec/models/bulk_imports/exports/group_config_spec.rb b/spec/models/bulk_imports/exports/group_config_spec.rb new file mode 100644 index 00000000000..becc39273ce --- /dev/null +++ b/spec/models/bulk_imports/exports/group_config_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::Exports::GroupConfig do + let_it_be(:exportable) { create(:group) } + let_it_be(:hex) { '123' } + + before do + allow(SecureRandom).to receive(:hex).and_return(hex) + end + + subject { described_class.new(exportable) } + + describe '#exportable_tree' do + it 'returns exportable tree' do + expect_next_instance_of(::Gitlab::ImportExport::AttributesFinder) do |finder| + expect(finder).to receive(:find_root).with(:group).and_call_original + end + + expect(subject.exportable_tree).not_to be_empty + end + end + + describe '#export_path' do + it 'returns correct export path' do + expect(::Gitlab::ImportExport).to receive(:storage_path).and_return('storage_path') + + expect(subject.export_path).to eq("storage_path/#{exportable.full_path}/#{hex}") + end + end + + describe '#validate_user_permissions' do + let_it_be(:user) { create(:user) } + + context 'when user cannot admin project' do + it 'returns false' do + expect { subject.validate_user_permissions!(user) }.to raise_error(Gitlab::ImportExport::Error) + end + end + + context 'when user can admin project' do + it 'returns true' do + exportable.add_owner(user) + + expect(subject.validate_user_permissions!(user)).to eq(true) + end + end + end + + describe '#exportable_relations' do + it 'returns a list of top level exportable relations' do + expect(subject.exportable_relations).to include('milestones', 'badges', 'boards', 'labels') + end + end +end diff --git a/spec/models/bulk_imports/exports/project_config_spec.rb b/spec/models/bulk_imports/exports/project_config_spec.rb new file mode 100644 index 00000000000..7aa769b09fd --- /dev/null +++ b/spec/models/bulk_imports/exports/project_config_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::Exports::ProjectConfig do + let_it_be(:exportable) { create(:project) } + let_it_be(:hex) { '123' } + + before do + allow(SecureRandom).to receive(:hex).and_return(hex) + end + + subject { described_class.new(exportable) } + + describe '#exportable_tree' do + it 'returns exportable tree' do + expect_next_instance_of(::Gitlab::ImportExport::AttributesFinder) do |finder| + expect(finder).to receive(:find_root).with(:project).and_call_original + end + + expect(subject.exportable_tree).not_to be_empty + end + end + + describe '#export_path' do + it 'returns correct export path' do + expect(::Gitlab::ImportExport).to receive(:storage_path).and_return('storage_path') + + expect(subject.export_path).to eq("storage_path/#{exportable.disk_path}/#{hex}") + end + end + + describe '#validate_user_permissions' do + let_it_be(:user) { create(:user) } + + context 'when user cannot admin project' do + it 'returns false' do + expect { subject.validate_user_permissions!(user) }.to raise_error(Gitlab::ImportExport::Error) + end + end + + context 'when user can admin project' do + it 'returns true' do + exportable.add_maintainer(user) + + expect(subject.validate_user_permissions!(user)).to eq(true) + end + end + end + + describe '#exportable_relations' do + it 'returns a list of top level exportable relations' do + expect(subject.exportable_relations).to include('issues', 'labels', 'milestones', 'merge_requests') + end + end +end diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb index 3da372c86a1..e371dd7a043 100644 --- a/spec/requests/api/environments_spec.rb +++ b/spec/requests/api/environments_spec.rb @@ -15,16 +15,6 @@ RSpec.describe API::Environments do describe 'GET /projects/:id/environments' do context 'as member of the project' do it 'returns project environments' do - project_data_keys = %w( - id description default_branch tag_list - ssh_url_to_repo http_url_to_repo web_url readme_url - name name_with_namespace - path path_with_namespace - star_count forks_count - created_at last_activity_at - avatar_url namespace - ) - get api("/projects/#{project.id}/environments", user) expect(response).to have_gitlab_http_status(:ok) @@ -33,7 +23,7 @@ RSpec.describe API::Environments do expect(json_response.size).to eq(1) expect(json_response.first['name']).to eq(environment.name) expect(json_response.first['external_url']).to eq(environment.external_url) - expect(json_response.first['project'].keys).to contain_exactly(*project_data_keys) + expect(json_response.first['project']).to match_schema('public_api/v4/project') expect(json_response.first['enable_advanced_logs_querying']).to eq(false) expect(json_response.first).not_to have_key('last_deployment') end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 144fee2e879..f293fb7f27b 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -348,22 +348,11 @@ RSpec.describe API::Projects do context 'and with simple=true' do it 'returns a simplified version of all the projects' do - expected_keys = %w( - id description default_branch tag_list - ssh_url_to_repo http_url_to_repo web_url readme_url - name name_with_namespace - path path_with_namespace - star_count forks_count - created_at last_activity_at - avatar_url namespace - ) - get api('/projects?simple=true', user) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.first.keys).to match_array expected_keys + expect(response).to match_response_schema('public_api/v4/projects') end end diff --git a/spec/services/clusters/management/create_project_service_spec.rb b/spec/services/clusters/management/create_project_service_spec.rb deleted file mode 100644 index 5d8cc71faa4..00000000000 --- a/spec/services/clusters/management/create_project_service_spec.rb +++ /dev/null @@ -1,126 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Clusters::Management::CreateProjectService do - let(:cluster) { create(:cluster, :project) } - let(:current_user) { create(:user) } - - subject { described_class.new(cluster, current_user: current_user).execute } - - shared_examples 'management project is not required' do - it 'does not create a project' do - expect { subject }.not_to change(cluster, :management_project) - end - end - - context ':auto_create_cluster_management_project feature flag is disabled' do - before do - stub_feature_flags(auto_create_cluster_management_project: false) - end - - include_examples 'management project is not required' - end - - context 'cluster already has a management project' do - let(:cluster) { create(:cluster, :management_project) } - - include_examples 'management project is not required' - end - - shared_examples 'creates a management project' do - let(:project_params) do - { - name: "#{cluster.name} Cluster Management", - description: 'This project is automatically generated and will be used to manage your Kubernetes cluster. [More information](/help/user/clusters/management_project)', - namespace_id: namespace&.id, - visibility_level: Gitlab::VisibilityLevel::PRIVATE - } - end - - it 'creates a management project' do - expect(Projects::CreateService).to receive(:new) - .with(current_user, project_params) - .and_call_original - - subject - - management_project = cluster.management_project - - expect(management_project).to be_present - expect(management_project).to be_private - expect(management_project.name).to eq "#{cluster.name} Cluster Management" - expect(management_project.namespace).to eq namespace - end - end - - context 'project cluster' do - let(:cluster) { create(:cluster, projects: [project]) } - let(:project) { create(:project, namespace: current_user.namespace) } - let(:namespace) { project.namespace } - - include_examples 'creates a management project' - end - - context 'group cluster' do - let(:cluster) { create(:cluster, :group, user: current_user) } - let(:namespace) { cluster.group } - - before do - namespace.add_user(current_user, Gitlab::Access::MAINTAINER) - end - - include_examples 'creates a management project' - end - - context 'instance cluster' do - let(:cluster) { create(:cluster, :instance, user: current_user) } - let(:namespace) { create(:group) } - - before do - stub_application_setting(instance_administrators_group: namespace) - - namespace.add_user(current_user, Gitlab::Access::MAINTAINER) - end - - include_examples 'creates a management project' - end - - describe 'error handling' do - let(:project) { cluster.project } - - before do - allow(Projects::CreateService).to receive(:new) - .and_return(double(execute: project)) - end - - context 'project is invalid' do - let(:errors) { double(full_messages: ["Error message"]) } - let(:project) { instance_double(Project, errors: errors) } - - it { expect { subject }.to raise_error(described_class::CreateError, /Failed to create project/) } - end - - context 'instance administrators group is missing' do - let(:cluster) { create(:cluster, :instance) } - - it { expect { subject }.to raise_error(described_class::CreateError, /Instance administrators group not found/) } - end - - context 'cluster is invalid' do - before do - allow(cluster).to receive(:update).and_return(false) - end - - it { expect { subject }.to raise_error(described_class::CreateError, /Failed to update cluster/) } - end - - context 'unknown cluster type' do - before do - allow(cluster).to receive(:cluster_type).and_return("unknown_type") - end - - it { expect { subject }.to raise_error(NotImplementedError) } - end - end -end diff --git a/spec/services/groups/open_issues_count_service_spec.rb b/spec/services/groups/open_issues_count_service_spec.rb index 740e9846119..fca09bfdebe 100644 --- a/spec/services/groups/open_issues_count_service_spec.rb +++ b/spec/services/groups/open_issues_count_service_spec.rb @@ -57,4 +57,15 @@ RSpec.describe Groups::OpenIssuesCountService, :use_clean_rails_memory_store_cac it_behaves_like 'a counter caching service with threshold' end end + + describe '#clear_all_cache_keys' do + it 'calls `Rails.cache.delete` with the correct keys' do + expect(Rails.cache).to receive(:delete) + .with(['groups', 'open_issues_count_service', 1, group.id, described_class::PUBLIC_COUNT_KEY]) + expect(Rails.cache).to receive(:delete) + .with(['groups', 'open_issues_count_service', 1, group.id, described_class::TOTAL_COUNT_KEY]) + + subject.clear_all_cache_keys + end + end end diff --git a/spec/services/issuable/bulk_update_service_spec.rb b/spec/services/issuable/bulk_update_service_spec.rb index c749f282cd3..dfdfb57111c 100644 --- a/spec/services/issuable/bulk_update_service_spec.rb +++ b/spec/services/issuable/bulk_update_service_spec.rb @@ -101,6 +101,22 @@ RSpec.describe Issuable::BulkUpdateService do end end + shared_examples 'scheduling cached group count clear' do + it 'schedules worker' do + expect(Issuables::ClearGroupsIssueCounterWorker).to receive(:perform_async) + + bulk_update(issuables, params) + end + end + + shared_examples 'not scheduling cached group count clear' do + it 'does not schedule worker' do + expect(Issuables::ClearGroupsIssueCounterWorker).not_to receive(:perform_async) + + bulk_update(issuables, params) + end + end + context 'with issuables at a project level' do let(:parent) { project } @@ -131,6 +147,11 @@ RSpec.describe Issuable::BulkUpdateService do expect(project.issues.opened).to be_empty expect(project.issues.closed).not_to be_empty end + + it_behaves_like 'scheduling cached group count clear' do + let(:issuables) { issues } + let(:params) { { state_event: 'close' } } + end end describe 'reopen issues' do @@ -149,6 +170,11 @@ RSpec.describe Issuable::BulkUpdateService do expect(project.issues.closed).to be_empty expect(project.issues.opened).not_to be_empty end + + it_behaves_like 'scheduling cached group count clear' do + let(:issuables) { issues } + let(:params) { { state_event: 'reopen' } } + end end describe 'updating merge request assignee' do @@ -231,6 +257,10 @@ RSpec.describe Issuable::BulkUpdateService do let(:milestone) { create(:milestone, project: project) } it_behaves_like 'updates milestones' + + it_behaves_like 'not scheduling cached group count clear' do + let(:params) { { milestone_id: milestone.id } } + end end describe 'updating labels' do diff --git a/spec/services/labels/available_labels_service_spec.rb b/spec/services/labels/available_labels_service_spec.rb index 9ee0b80edcd..355dbd0c712 100644 --- a/spec/services/labels/available_labels_service_spec.rb +++ b/spec/services/labels/available_labels_service_spec.rb @@ -36,6 +36,15 @@ RSpec.describe Labels::AvailableLabelsService do expect(result).to include(project_label, group_label) expect(result).not_to include(other_project_label, other_group_label) end + + it 'do not cause additional query for finding labels' do + label_titles = [project_label.title] + control_count = ActiveRecord::QueryRecorder.new { described_class.new(user, project, labels: label_titles).find_or_create_by_titles } + + new_label = create(:label, project: project) + label_titles = [project_label.title, new_label.title] + expect { described_class.new(user, project, labels: label_titles).find_or_create_by_titles }.not_to exceed_query_limit(control_count) + end end end diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 6eff768eac2..c3a0766cb17 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -412,7 +412,7 @@ RSpec.describe NotificationService, :mailer do it_should_not_email! end - context 'do exist' do + context 'do exist and note not confidential' do let!(:issue_email_participant) { issue.issue_email_participants.create!(email: 'service.desk@example.com') } before do @@ -422,6 +422,18 @@ RSpec.describe NotificationService, :mailer do it_should_email! end + + context 'do exist and note is confidential' do + let(:note) { create(:note, noteable: issue, project: project, confidential: true) } + let!(:issue_email_participant) { issue.issue_email_participants.create!(email: 'service.desk@example.com') } + + before do + issue.update!(external_author: 'service.desk@example.com') + project.update!(service_desk_enabled: true) + end + + it_should_not_email! + end end describe '#new_note' do diff --git a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb index 72b5b31bb7a..ea0bdb1a9ae 100644 --- a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb +++ b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb @@ -56,6 +56,9 @@ RSpec.describe 'layouts/nav/sidebar/_project' do describe 'Learn GitLab' do it 'has a link to the learn GitLab experiment' do allow(view).to receive(:learn_gitlab_experiment_enabled?).and_return(true) + allow_next_instance_of(LearnGitlab::Onboarding) do |onboarding| + expect(onboarding).to receive(:completed_percentage).and_return(20) + end render diff --git a/spec/views/projects/settings/operations/show.html.haml_spec.rb b/spec/views/projects/settings/operations/show.html.haml_spec.rb index e6d53c526e2..ab868eb78b8 100644 --- a/spec/views/projects/settings/operations/show.html.haml_spec.rb +++ b/spec/views/projects/settings/operations/show.html.haml_spec.rb @@ -47,7 +47,7 @@ RSpec.describe 'projects/settings/operations/show' do render expect(rendered).to have_content _('Error tracking') - expect(rendered).to have_content _('To link Sentry to GitLab, enter your Sentry URL and Auth Token') + expect(rendered).to have_content _('Link Sentry to GitLab to discover and view the errors your application generates.') end end end diff --git a/spec/workers/issuables/clear_groups_issue_counter_worker_spec.rb b/spec/workers/issuables/clear_groups_issue_counter_worker_spec.rb new file mode 100644 index 00000000000..ac430f42e7a --- /dev/null +++ b/spec/workers/issuables/clear_groups_issue_counter_worker_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Issuables::ClearGroupsIssueCounterWorker do + describe '#perform' do + let_it_be(:user) { create(:user) } + let_it_be(:parent_group) { create(:group) } + let_it_be(:root_group) { create(:group, parent: parent_group) } + let_it_be(:subgroup) { create(:group, parent: root_group) } + + let(:count_service) { Groups::OpenIssuesCountService } + let(:instance1) { instance_double(count_service) } + let(:instance2) { instance_double(count_service) } + + it_behaves_like 'an idempotent worker' do + let(:job_args) { [[root_group.id]] } + let(:exec_times) { IdempotentWorkerHelper::WORKER_EXEC_TIMES } + + it 'clears the cached issue count in given groups and ancestors' do + expect(count_service).to receive(:new) + .exactly(exec_times).times.with(root_group).and_return(instance1) + expect(count_service).to receive(:new) + .exactly(exec_times).times.with(parent_group).and_return(instance2) + expect(count_service).not_to receive(:new).with(subgroup) + + [instance1, instance2].all? do |instance| + expect(instance).to receive(:clear_all_cache_keys).exactly(exec_times).times + end + + subject + end + end + + it 'does not call count service or rise error when group_ids is empty' do + expect(count_service).not_to receive(:new) + expect(Gitlab::ErrorTracking).not_to receive(:log_exception) + + described_class.new.perform([]) + end + end +end diff --git a/yarn.lock b/yarn.lock index 3f36491fb2d..ee8ab91b018 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2105,11 +2105,6 @@ ajv@^7.0.2: require-from-string "^2.0.2" uri-js "^4.2.2" -amdefine@>=0.0.4: - version "1.0.1" - resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" - integrity sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU= - ansi-align@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.0.tgz#b536b371cf687caaef236c18d3e21fe3797467cb" @@ -2159,11 +2154,6 @@ ansi-regex@^5.0.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== -ansi-styles@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" - integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= - ansi-styles@^3.2.0, ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" @@ -2301,19 +2291,11 @@ append-transform@^1.0.0: dependencies: default-require-extensions "^2.0.0" -aproba@^1.0.3, aproba@^1.1.1: +aproba@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== -are-we-there-yet@~1.1.2: - version "1.1.5" - resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" - integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w== - dependencies: - delegates "^1.0.0" - readable-stream "^2.0.6" - argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -2349,11 +2331,6 @@ arr-union@^3.1.0: resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= -array-find-index@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" - integrity sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E= - array-find@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/array-find/-/array-find-1.0.0.tgz#6c8e286d11ed768327f8e62ecee87353ca3e78b8" @@ -2459,11 +2436,6 @@ astral-regex@^2.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== -async-foreach@^0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542" - integrity sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI= - async-limiter@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" @@ -2730,13 +2702,6 @@ blob@0.0.4: resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.4.tgz#bcf13052ca54463f30f9fc7e95b9a47630a94921" integrity sha1-vPEwUspURj8w+fx+lbmkdjCpSSE= -block-stream@*: - version "0.0.9" - resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" - integrity sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo= - dependencies: - inherits "~2.0.0" - bluebird@^3.1.1, bluebird@^3.3.0, bluebird@^3.5.5, bluebird@~3.5.0: version "3.5.5" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.5.tgz#a8d0afd73251effbbd5fe384a77d73003c17a71f" @@ -3081,14 +3046,6 @@ camel-case@^3.0.0: no-case "^2.2.0" upper-case "^1.1.1" -camelcase-keys@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" - integrity sha1-MIvur/3ygRkFHvodkyITyRuPkuc= - dependencies: - camelcase "^2.0.0" - map-obj "^1.0.0" - camelcase-keys@^6.2.2: version "6.2.2" resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-6.2.2.tgz#5e755d6ba51aa223ec7d3d52f25778210f9dc3c0" @@ -3098,11 +3055,6 @@ camelcase-keys@^6.2.2: map-obj "^4.0.0" quick-lru "^4.0.1" -camelcase@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" - integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8= - camelcase@^5.0.0, camelcase@^5.2.0, camelcase@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" @@ -3137,17 +3089,6 @@ catharsis@~0.8.9: dependencies: underscore-contrib "~0.3.0" -chalk@^1.1.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" - integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= - dependencies: - ansi-styles "^2.2.1" - escape-string-regexp "^1.0.2" - has-ansi "^2.0.0" - strip-ansi "^3.0.0" - supports-color "^2.0.0" - chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -3198,7 +3139,7 @@ charenc@~0.0.1: resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc= -chokidar@^2.1.8, chokidar@^3.0.0, chokidar@^3.2.2, chokidar@^3.4.0, chokidar@^3.4.1: +"chokidar@>=3.0.0 <4.0.0", chokidar@^2.1.8, chokidar@^3.0.0, chokidar@^3.2.2, chokidar@^3.4.0, chokidar@^3.4.1: version "3.4.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.0.tgz#b30611423ce376357c765b9b8f904b9fba3c0be8" integrity sha512-aXAaho2VJtisB/1fg1+3nlLJqGOuewTzQpd/Tz0yTg2R0e4IGtshYvtjowyEumcBv2z+y4+kc75Mz7j5xJskcQ== @@ -3334,11 +3275,6 @@ co@^4.6.0: resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= -code-point-at@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" - integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= - codemirror@^5.48.4: version "5.53.2" resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.53.2.tgz#9799121cf8c50809cca487304e9de3a74d33f428" @@ -3564,11 +3500,6 @@ console-browserify@^1.1.0: dependencies: date-now "^0.1.4" -console-control-strings@^1.0.0, console-control-strings@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" - integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= - consolidate@^0.15.1: version "0.15.1" resolved "https://registry.yarnpkg.com/consolidate/-/consolidate-0.15.1.tgz#21ab043235c71a07d45d9aad98593b0dba56bab7" @@ -3742,14 +3673,6 @@ cropper@^2.3.0: dependencies: jquery ">= 1.9.1" -cross-spawn@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-3.0.1.tgz#1256037ecb9f0c5f79e3d6ef135e30770184b982" - integrity sha1-ElYDfsufDF9549bvE14wdwGEuYI= - dependencies: - lru-cache "^4.0.1" - which "^1.2.9" - cross-spawn@^6.0.0, cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -3880,13 +3803,6 @@ cssstyle@^2.2.0: dependencies: cssom "~0.3.6" -currently-unhandled@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" - integrity sha1-mI3zP+qxke95mmE2nddsF635V+o= - dependencies: - array-find-index "^1.0.1" - custom-event@~1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425" @@ -4248,7 +4164,7 @@ decamelize-keys@^1.1.0: decamelize "^1.1.0" map-obj "^1.0.0" -decamelize@^1.1.0, decamelize@^1.1.2, decamelize@^1.2.0: +decamelize@^1.1.0, decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= @@ -4367,11 +4283,6 @@ delegate@^3.1.2: resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.1.2.tgz#1e1bc6f5cadda6cb6cbf7e6d05d0bcdd5712aebe" integrity sha1-HhvG9crdpstsv35tBdC83VcSrr4= -delegates@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" - integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= - depd@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359" @@ -4852,7 +4763,7 @@ escape-html@~1.0.3: resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= -escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5, escape-string-regexp@~1.0.5: +escape-string-regexp@^1.0.5, escape-string-regexp@~1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= @@ -5499,14 +5410,6 @@ find-root@^1.1.0: resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng== -find-up@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" - integrity sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8= - dependencies: - path-exists "^2.0.0" - pinkie-promise "^2.0.0" - find-up@^2.0.0, find-up@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" @@ -5655,16 +5558,6 @@ fsevents@^2.1.2, fsevents@~2.1.2: resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== -fstream@^1.0.0, fstream@^1.0.12: - version "1.0.12" - resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045" - integrity sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg== - dependencies: - graceful-fs "^4.1.2" - inherits "~2.0.0" - mkdirp ">=0.5 0" - rimraf "2" - function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" @@ -5680,27 +5573,6 @@ fuzzaldrin-plus@^0.6.0: resolved "https://registry.yarnpkg.com/fuzzaldrin-plus/-/fuzzaldrin-plus-0.6.0.tgz#832f6489fbe876769459599c914a670ec22947ee" integrity sha1-gy9kifvodnaUWVmckUpnDsIpR+4= -gauge@~2.7.3: - version "2.7.4" - resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" - integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c= - dependencies: - aproba "^1.0.3" - console-control-strings "^1.0.0" - has-unicode "^2.0.0" - object-assign "^4.1.0" - signal-exit "^3.0.0" - string-width "^1.0.1" - strip-ansi "^3.0.1" - wide-align "^1.1.0" - -gaze@^1.0.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/gaze/-/gaze-1.1.3.tgz#c441733e13b927ac8c0ff0b4c3b033f28812924a" - integrity sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g== - dependencies: - globule "^1.0.0" - gensync@^1.0.0-beta.1: version "1.0.0-beta.1" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269" @@ -5725,11 +5597,6 @@ get-package-type@^0.1.0: resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== -get-stdin@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" - integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4= - get-stdin@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-6.0.0.tgz#9e09bf712b360ab9225e812048f71fde9c89657b" @@ -5798,7 +5665,7 @@ glob-to-regexp@^0.4.0: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== -"glob@5 - 7", glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@~7.1.1, glob@~7.1.6: +"glob@5 - 7", glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@~7.1.6: version "7.1.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== @@ -5908,15 +5775,6 @@ globjoin@^0.1.4: resolved "https://registry.yarnpkg.com/globjoin/-/globjoin-0.1.4.tgz#2f4494ac8919e3767c5cbb691e9f463324285d43" integrity sha1-L0SUrIkZ43Z8XLtpHp9GMyQoXUM= -globule@^1.0.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/globule/-/globule-1.3.2.tgz#d8bdd9e9e4eef8f96e245999a5dee7eb5d8529c4" - integrity sha512-7IDTQTIu2xzXkT+6mlluidnWo+BypnbSoEVVQCGfzqnl5Ik8d3e1d4wycb8Rj9tWW+Z39uPWsdlquqiqPCd/pA== - dependencies: - glob "~7.1.1" - lodash "~4.17.10" - minimatch "~3.0.2" - gonzales-pe@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/gonzales-pe/-/gonzales-pe-4.3.0.tgz#fe9dec5f3c557eead09ff868c65826be54d067b3" @@ -6016,13 +5874,6 @@ hard-rejection@^2.1.0: resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883" integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA== -has-ansi@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" - integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= - dependencies: - ansi-regex "^2.0.0" - has-binary2@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-binary2/-/has-binary2-1.0.2.tgz#e83dba49f0b9be4d026d27365350d9f03f54be98" @@ -6050,11 +5901,6 @@ has-symbols@^1.0.0, has-symbols@^1.0.1: resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== -has-unicode@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" - integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= - has-value@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" @@ -6404,18 +6250,6 @@ imurmurhash@^0.1.4: resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= -in-publish@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/in-publish/-/in-publish-2.0.1.tgz#948b1a535c8030561cea522f73f78f4be357e00c" - integrity sha512-oDM0kUSNFC31ShNxHKUyfZKy8ZeXZBWMjMdZHKLOk13uvT27VTL/QzRGfRUcevJhpkZAvlhPYuXkF7eNWrtyxQ== - -indent-string@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" - integrity sha1-ji1INIdCEhtKghi3oTfppSBJ3IA= - dependencies: - repeating "^2.0.0" - indent-string@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" @@ -6449,7 +6283,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3: +inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -6639,18 +6473,6 @@ is-extglob@^2.1.1: resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= -is-finite@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.1.0.tgz#904135c77fb42c0641d6aa1bcdbc4daa8da082f3" - integrity sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w== - -is-fullwidth-code-point@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" - integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= - dependencies: - number-is-nan "^1.0.0" - is-fullwidth-code-point@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" @@ -6799,11 +6621,6 @@ is-typedarray@^1.0.0, is-typedarray@~1.0.0: resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= -is-utf8@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" - integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= - is-whitespace@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/is-whitespace/-/is-whitespace-0.3.0.tgz#1639ecb1be036aec69a54cbb401cfbed7114ab7f" @@ -7430,11 +7247,6 @@ jquery.waitforimages@^2.2.0: resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.5.1.tgz#d7b4d08e1bfdb86ad2f1a3d039ea17304717abb5" integrity sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg== -js-base64@^2.1.8: - version "2.6.4" - resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.6.4.tgz#f4e686c5de1ea1f867dbcad3d46d969428df98c4" - integrity sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ== - js-beautify@^1.6.12, js-beautify@^1.8.8: version "1.11.0" resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.11.0.tgz#afb873dc47d58986360093dcb69951e8bcd5ded2" @@ -7863,17 +7675,6 @@ linkify-it@^3.0.1: dependencies: uc.micro "^1.0.1" -load-json-file@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" - integrity sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA= - dependencies: - graceful-fs "^4.1.2" - parse-json "^2.2.0" - pify "^2.0.0" - pinkie-promise "^2.0.0" - strip-bom "^2.0.0" - load-json-file@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8" @@ -8065,7 +7866,7 @@ lodash.values@^4.3.0: resolved "https://registry.yarnpkg.com/lodash.values/-/lodash.values-4.3.0.tgz#a3a6c2b0ebecc5c2cba1c17e6e620fe81b53d347" integrity sha1-o6bCsOvsxcLLocF+bmIP6BtT00c= -lodash@^4.0.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@~4.17.10: +lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -8112,14 +7913,6 @@ loose-envify@^1.0.0: dependencies: js-tokens "^3.0.0 || ^4.0.0" -loud-rejection@^1.0.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" - integrity sha1-W0b4AUft7leIcPCG0Eghz5mOVR8= - dependencies: - currently-unhandled "^0.4.1" - signal-exit "^3.0.0" - lower-case@^1.1.1: version "1.1.4" resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac" @@ -8143,7 +7936,7 @@ lowlight@^1.17.0, lowlight@^1.20.0: fault "^1.0.0" highlight.js "~10.7.0" -lru-cache@4.1.x, lru-cache@^4.0.1, lru-cache@^4.1.2, lru-cache@^4.1.5: +lru-cache@4.1.x, lru-cache@^4.1.2, lru-cache@^4.1.5: version "4.1.5" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== @@ -8197,7 +7990,7 @@ map-cache@^0.2.2: resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= -map-obj@^1.0.0, map-obj@^1.0.1: +map-obj@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0= @@ -8359,22 +8152,6 @@ memory-fs@^0.5.0: errno "^0.1.3" readable-stream "^2.0.1" -meow@^3.7.0: - version "3.7.0" - resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" - integrity sha1-cstmi0JSKCkKu/qFaJJYcwioAfs= - dependencies: - camelcase-keys "^2.0.0" - decamelize "^1.1.2" - loud-rejection "^1.0.0" - map-obj "^1.0.1" - minimist "^1.1.3" - normalize-package-data "^2.3.4" - object-assign "^4.0.1" - read-pkg-up "^1.0.1" - redent "^1.0.0" - trim-newlines "^1.0.0" - meow@^9.0.0: version "9.0.0" resolved "https://registry.yarnpkg.com/meow/-/meow-9.0.0.tgz#cd9510bc5cac9dee7d03c73ee1f9ad959f4ea364" @@ -8547,7 +8324,7 @@ minimalistic-crypto-utils@^1.0.1: resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= -minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4, minimatch@~3.0.2, minimatch@~3.0.4: +minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4, minimatch@~3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== @@ -8563,7 +8340,7 @@ minimist-options@4.1.0: is-plain-obj "^1.1.0" kind-of "^6.0.3" -minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5, minimist@~1.2.5: +minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5, minimist@~1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== @@ -8665,7 +8442,7 @@ mixin-deep@^1.2.0: for-in "^1.0.2" is-extendable "^1.0.1" -"mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.5, mkdirp@~0.5.1: +mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.5, mkdirp@~0.5.1: version "0.5.5" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== @@ -8751,11 +8528,6 @@ multicast-dns@^6.0.1: dns-packet "^1.0.1" thunky "^0.1.0" -nan@^2.13.2: - version "2.14.1" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01" - integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw== - nanomatch@^1.2.9: version "1.2.13" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" @@ -8815,24 +8587,6 @@ node-forge@^0.10.0: resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3" integrity sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA== -node-gyp@^3.8.0: - version "3.8.0" - resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.8.0.tgz#540304261c330e80d0d5edce253a68cb3964218c" - integrity sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA== - dependencies: - fstream "^1.0.0" - glob "^7.0.3" - graceful-fs "^4.1.2" - mkdirp "^0.5.0" - nopt "2 || 3" - npmlog "0 || 1 || 2 || 3 || 4" - osenv "0" - request "^2.87.0" - rimraf "2" - semver "~5.3.0" - tar "^2.0.0" - which "1" - node-int64@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" @@ -8889,29 +8643,6 @@ node-releases@^1.1.69: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.70.tgz#66e0ed0273aa65666d7fe78febe7634875426a08" integrity sha512-Slf2s69+2/uAD79pVVQo8uSiC34+g8GWY8UH2Qtqv34ZfhYrxpYpfzs9Js9d6O0mbDmALuxaTlplnBTnSELcrw== -node-sass@^4.14.1: - version "4.14.1" - resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.14.1.tgz#99c87ec2efb7047ed638fb4c9db7f3a42e2217b5" - integrity sha512-sjCuOlvGyCJS40R8BscF5vhVlQjNN069NtQ1gSxyK1u9iqvn6tf7O1R4GNowVZfiZUCRt5MmMs1xd+4V/7Yr0g== - dependencies: - async-foreach "^0.1.3" - chalk "^1.1.1" - cross-spawn "^3.0.0" - gaze "^1.0.0" - get-stdin "^4.0.1" - glob "^7.0.3" - in-publish "^2.0.0" - lodash "^4.17.15" - meow "^3.7.0" - mkdirp "^0.5.1" - nan "^2.13.2" - node-gyp "^3.8.0" - npmlog "^4.0.0" - request "^2.88.0" - sass-graph "2.2.5" - stdout-stream "^1.4.0" - "true-case-path" "^1.0.2" - nodemon@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.4.tgz#55b09319eb488d6394aa9818148c0c2d1c04c416" @@ -8928,13 +8659,6 @@ nodemon@^2.0.4: undefsafe "^2.0.2" update-notifier "^4.0.0" -"nopt@2 || 3": - version "3.0.6" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" - integrity sha1-xkZdvwirzU2zWTF/eaxopkayj/k= - dependencies: - abbrev "1" - nopt@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.3.tgz#a375cad9d02fd921278d954c2254d5aa57e15e48" @@ -8950,7 +8674,7 @@ nopt@~1.0.10: dependencies: abbrev "1" -normalize-package-data@^2.3.2, normalize-package-data@^2.3.4, normalize-package-data@^2.5.0: +normalize-package-data@^2.3.2, normalize-package-data@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== @@ -9011,26 +8735,11 @@ npm-run-path@^4.0.0: dependencies: path-key "^3.0.0" -"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.0: - version "4.1.2" - resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" - integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== - dependencies: - are-we-there-yet "~1.1.2" - console-control-strings "~1.1.0" - gauge "~2.7.3" - set-blocking "~2.0.0" - num2fraction@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" integrity sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4= -number-is-nan@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" - integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= - nwsapi@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7" @@ -9041,7 +8750,7 @@ oauth-sign@~0.9.0: resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== -object-assign@^4.0.1, object-assign@^4.1.0: +object-assign@^4.0.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= @@ -9223,7 +8932,7 @@ os-tmpdir@^1.0.0, os-tmpdir@~1.0.2: resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= -osenv@0, osenv@^0.1.4: +osenv@^0.1.4: version "0.1.5" resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g== @@ -9441,13 +9150,6 @@ path-browserify@0.0.1: resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.1.tgz#e6c4ddd7ed3aa27c68a20cc4e50e1a4ee83bbc4a" integrity sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ== -path-exists@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" - integrity sha1-D+tsZPD8UY2adU3V77YscCJ2H0s= - dependencies: - pinkie-promise "^2.0.0" - path-exists@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" @@ -9488,15 +9190,6 @@ path-to-regexp@0.1.7: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= -path-type@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" - integrity sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE= - dependencies: - graceful-fs "^4.1.2" - pify "^2.0.0" - pinkie-promise "^2.0.0" - path-type@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" @@ -10135,14 +9828,6 @@ react-is@^16.12.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== -read-pkg-up@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" - integrity sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI= - dependencies: - find-up "^1.0.0" - read-pkg "^1.0.0" - read-pkg-up@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" @@ -10160,15 +9845,6 @@ read-pkg-up@^7.0.1: read-pkg "^5.2.0" type-fest "^0.8.1" -read-pkg@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" - integrity sha1-9f+qXs0pyzHAR0vKfXVra7KePyg= - dependencies: - load-json-file "^1.0.0" - normalize-package-data "^2.3.2" - path-type "^1.0.0" - read-pkg@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8" @@ -10188,7 +9864,7 @@ read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" -"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.6: +"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.6: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -10237,14 +9913,6 @@ readdirp@~3.4.0: dependencies: picomatch "^2.2.1" -redent@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" - integrity sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94= - dependencies: - indent-string "^2.1.0" - strip-indent "^1.0.1" - redent@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" @@ -10372,13 +10040,6 @@ repeat-string@^1.0.0, repeat-string@^1.6.1: resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= -repeating@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" - integrity sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo= - dependencies: - is-finite "^1.0.0" - request-light@^0.2.4: version "0.2.5" resolved "https://registry.yarnpkg.com/request-light/-/request-light-0.2.5.tgz#38a3da7b2e56f7af8cbba57e8a94930ee2380746" @@ -10404,7 +10065,7 @@ request-promise-native@^1.0.8: stealthy-require "^1.1.1" tough-cookie "^2.3.3" -request@^2.87.0, request@^2.88.0, request@^2.88.2: +request@^2.88.2: version "2.88.2" resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== @@ -10543,7 +10204,7 @@ rfdc@^1.1.4: resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.1.4.tgz#ba72cc1367a0ccd9cf81a870b3b58bd3ad07f8c2" integrity sha512-5C9HXdzK8EAqN7JDif30jqsBzavB7wLpaubisuQIGHWf2gUXSpzy6ArX/+Da8RjFpagWsCn+pIgxTMAmKw9Zug== -rimraf@2, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.3: +rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.3: version "2.6.3" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== @@ -10629,15 +10290,12 @@ sane@^4.0.3: minimist "^1.1.1" walker "~1.0.5" -sass-graph@2.2.5: - version "2.2.5" - resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.5.tgz#a981c87446b8319d96dce0671e487879bd24c2e8" - integrity sha512-VFWDAHOe6mRuT4mZRd4eKE+d8Uedrk6Xnh7Sh9b4NGufQLQjOrvf/MQoOdx+0s92L89FeyUUNfU597j/3uNpag== +sass@^1.32.12: + version "1.32.12" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.32.12.tgz#a2a47ad0f1c168222db5206444a30c12457abb9f" + integrity sha512-zmXn03k3hN0KaiVTjohgkg98C3UowhL1/VSGdj4/VAAiMKGQOE80PFPxFP2Kyq0OUskPKcY5lImkhBKEHlypJA== dependencies: - glob "^7.0.0" - lodash "^4.0.0" - scss-tokenizer "^0.2.3" - yargs "^13.3.2" + chokidar ">=3.0.0 <4.0.0" sax@1.2.1, sax@>=0.6.0: version "1.2.1" @@ -10686,14 +10344,6 @@ schema-utils@^3.0.0: ajv "^6.12.5" ajv-keywords "^3.5.2" -scss-tokenizer@^0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz#8eb06db9a9723333824d3f5530641149847ce5d1" - integrity sha1-jrBtualyMzOCTT9VMGQRSYR85dE= - dependencies: - js-base64 "^2.1.8" - source-map "^0.4.2" - select-hose@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" @@ -10745,11 +10395,6 @@ semver@^7.2.1, semver@^7.3.2: dependencies: lru-cache "^6.0.0" -semver@~5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" - integrity sha1-myzl094C0XxgEq0yaqa00M9U+U8= - send@0.17.1: version "0.17.1" resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" @@ -10811,7 +10456,7 @@ serve-static@1.14.1: parseurl "~1.3.3" send "0.17.1" -set-blocking@^2.0.0, set-blocking@~2.0.0: +set-blocking@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= @@ -11075,13 +10720,6 @@ source-map@0.5.6, source-map@^0.5.0, source-map@^0.5.6: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" integrity sha1-dc449SvwczxafwwRjYEzSiu19BI= -source-map@^0.4.2: - version "0.4.4" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b" - integrity sha1-66T12pwNyZneaAMti092FzZSA2s= - dependencies: - amdefine ">=0.0.4" - source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" @@ -11217,13 +10855,6 @@ statuses@~1.3.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" integrity sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4= -stdout-stream@^1.4.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/stdout-stream/-/stdout-stream-1.4.1.tgz#5ac174cdd5cd726104aa0c0b2bd83815d8d535de" - integrity sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA== - dependencies: - readable-stream "^2.0.1" - stealthy-require@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" @@ -11285,15 +10916,6 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -string-width@^1.0.1, "string-width@^1.0.2 || 2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" - integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= - dependencies: - code-point-at "^1.0.0" - is-fullwidth-code-point "^1.0.0" - strip-ansi "^3.0.0" - string-width@^3.0.0, string-width@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" @@ -11340,7 +10962,7 @@ string_decoder@~0.10.x: resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= -strip-ansi@^3.0.0, strip-ansi@^3.0.1: +strip-ansi@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= @@ -11368,13 +10990,6 @@ strip-ansi@^6.0.0: dependencies: ansi-regex "^5.0.0" -strip-bom@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" - integrity sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4= - dependencies: - is-utf8 "^0.2.0" - strip-bom@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" @@ -11395,13 +11010,6 @@ strip-final-newline@^2.0.0: resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== -strip-indent@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" - integrity sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI= - dependencies: - get-stdin "^4.0.1" - strip-indent@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" @@ -11517,11 +11125,6 @@ sugarss@^2.0.0: dependencies: postcss "^7.0.2" -supports-color@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" - integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= - supports-color@^5.3.0, supports-color@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" @@ -11596,15 +11199,6 @@ tapable@^1.0.0, tapable@^1.1.3: resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== -tar@^2.0.0: - version "2.2.2" - resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.2.tgz#0ca8848562c7299b8b446ff6a4d60cdbb23edc40" - integrity sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA== - dependencies: - block-stream "*" - fstream "^1.0.12" - inherits "2" - tar@^6.0.2: version "6.0.5" resolved "https://registry.yarnpkg.com/tar/-/tar-6.0.5.tgz#bde815086e10b39f1dcd298e89d596e1535e200f" @@ -11905,11 +11499,6 @@ tr46@^2.0.2: dependencies: punycode "^2.1.1" -trim-newlines@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" - integrity sha1-WIeWa7WCpFA6QetST301ARgVphM= - trim-newlines@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.0.tgz#79726304a6a898aa8373427298d54c2ee8b1cb30" @@ -11920,13 +11509,6 @@ trough@^1.0.0: resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406" integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA== -"true-case-path@^1.0.2": - version "1.0.3" - resolved "https://registry.yarnpkg.com/true-case-path/-/true-case-path-1.0.3.tgz#f813b5a8c86b40da59606722b144e3225799f47d" - integrity sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew== - dependencies: - glob "^7.1.2" - try-catch@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/try-catch/-/try-catch-2.0.0.tgz#a491141d597f8b72b46757fe1c47059341a16aed" @@ -12820,7 +12402,7 @@ which-module@^2.0.0: resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= -which@1, which@^1.2.1, which@^1.2.14, which@^1.2.9, which@^1.3.1: +which@^1.2.1, which@^1.2.14, which@^1.2.9, which@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== @@ -12834,13 +12416,6 @@ which@^2.0.1, which@^2.0.2: dependencies: isexe "^2.0.0" -wide-align@^1.1.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" - integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== - dependencies: - string-width "^1.0.2 || 2" - widest-line@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca" |
