diff --git a/app/assets/javascripts/repository/components/header_area/blob_button_group.vue b/app/assets/javascripts/repository/components/header_area/blob_button_group.vue
index c233bd2823da68a981d6d6695e2205df62578206..af7720374e348aaf3c4c74d4cc1d644d9d8f5edd 100644
--- a/app/assets/javascripts/repository/components/header_area/blob_button_group.vue
+++ b/app/assets/javascripts/repository/components/header_area/blob_button_group.vue
@@ -1,34 +1,33 @@
+
+
+
+
+
+
+
diff --git a/app/assets/javascripts/repository/components/header_area/blob_overflow_menu.vue b/app/assets/javascripts/repository/components/header_area/blob_overflow_menu.vue
index fa6954d011d94e80a40fde56f89de565332097a9..9f34dd7f843e080195a0312062e8ba7ff0a997ca 100644
--- a/app/assets/javascripts/repository/components/header_area/blob_overflow_menu.vue
+++ b/app/assets/javascripts/repository/components/header_area/blob_overflow_menu.vue
@@ -2,13 +2,20 @@
import { GlDisclosureDropdown, GlTooltipDirective } from '@gitlab/ui';
import { computed } from 'vue';
import { sprintf, s__, __ } from '~/locale';
+import { createAlert } from '~/alert';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { SIMPLE_BLOB_VIEWER, RICH_BLOB_VIEWER } from '~/blob/components/constants';
+import { DEFAULT_BLOB_INFO } from '~/repository/constants';
+import getRefMixin from '~/repository/mixins/get_ref';
+import projectInfoQuery from 'ee_else_ce/repository/queries/project_info.query.graphql';
import BlobDefaultActionsGroup from './blob_default_actions_group.vue';
import BlobButtonGroup from './blob_button_group.vue';
+import BlobDeleteFileGroup from './blob_delete_file_group.vue';
export const i18n = {
dropdownLabel: __('Actions'),
+ delete: __('Delete file'),
+ fetchError: __('An error occurred while fetching lock information, please try again.'),
};
export default {
@@ -17,71 +24,38 @@ export default {
GlDisclosureDropdown,
BlobDefaultActionsGroup,
BlobButtonGroup,
+ BlobDeleteFileGroup,
},
directives: {
GlTooltipDirective,
},
- inject: ['canModifyBlob', 'canModifyBlobWithWebIde'],
+ mixins: [getRefMixin],
+ inject: {
+ targetBranch: {
+ default: '',
+ },
+ originalBranch: {
+ default: '',
+ },
+ blobInfo: {
+ default: () => DEFAULT_BLOB_INFO.repository.blobs.nodes[0],
+ },
+ },
provide() {
return {
- canModifyBlob: computed(() => this.canModifyBlob),
- canModifyBlobWithWebIde: computed(() => this.canModifyBlobWithWebIde),
+ blobInfo: computed(() => this.blobInfo ?? {}),
};
},
props: {
- name: {
- type: String,
- required: true,
- },
- archived: {
- type: Boolean,
- required: true,
- },
projectPath: {
type: String,
required: true,
},
- path: {
- type: String,
- required: true,
- },
- rawPath: {
- type: String,
- required: true,
- },
- replacePath: {
- type: String,
- required: true,
- },
- webPath: {
- type: String,
- required: true,
- },
- richViewer: {
- type: Object,
- required: false,
- default: () => {},
- },
- simpleViewer: {
- type: Object,
- required: false,
- default: () => {},
- },
isBinary: {
type: Boolean,
required: false,
default: false,
},
- environmentName: {
- type: String,
- required: false,
- default: null,
- },
- environmentPath: {
- type: String,
- required: false,
- default: null,
- },
isEmpty: {
type: Boolean,
required: false,
@@ -97,22 +71,48 @@ export default {
required: false,
default: false,
},
- canCurrentUserPushToBranch: {
- type: Boolean,
- required: true,
- },
isUsingLfs: {
type: Boolean,
required: false,
default: false,
},
},
+ apollo: {
+ // eslint-disable-next-line @gitlab/vue-no-undef-apollo-properties
+ projectInfo: {
+ query: projectInfoQuery,
+ variables() {
+ return {
+ projectPath: this.projectPath,
+ };
+ },
+ update({ project }) {
+ this.pathLocks = project?.pathLocks || DEFAULT_BLOB_INFO.pathLocks;
+ this.userPermissions = project?.userPermissions;
+ },
+ error() {
+ createAlert({ message: this.$options.i18n.fetchError });
+ },
+ },
+ },
data() {
return {
+ userPermissions: DEFAULT_BLOB_INFO.userPermissions,
isLoggedIn: isLoggedIn(),
};
},
computed: {
+ isLoading() {
+ return this.$apollo?.queries.projectInfo.loading;
+ },
+ deleteFileItem() {
+ return {
+ text: this.$options.i18n.delete,
+ extraAttrs: {
+ 'data-testid': 'delete',
+ },
+ };
+ },
activeViewerType() {
if (this.$route?.query?.plain !== '1') {
const richViewer = document.querySelector('.blob-viewer[data-type="rich"]');
@@ -123,14 +123,16 @@ export default {
return SIMPLE_BLOB_VIEWER;
},
viewer() {
- return this.activeViewerType === RICH_BLOB_VIEWER ? this.richViewer : this.simpleViewer;
+ return this.activeViewerType === RICH_BLOB_VIEWER
+ ? this.blobInfo.richViewer
+ : this.blobInfo.simpleViewer;
},
hasRenderError() {
return Boolean(this.viewer.renderError);
},
environmentTitle() {
return sprintf(s__('BlobViewer|View on %{environmentName}'), {
- environmentName: this.environmentName,
+ environmentName: this.blobInfo.environmentFormattedExternalUrl,
});
},
},
@@ -153,28 +155,28 @@ export default {
text-sr-only
>
+
diff --git a/app/assets/javascripts/repository/queries/blob_controls.query.graphql b/app/assets/javascripts/repository/queries/blob_controls.query.graphql
index 9c50ba6236b9df9f62b71e6c69b93214d2da9ec1..5751e39133668d06b1df2f4d7b77b51d63df6c0b 100644
--- a/app/assets/javascripts/repository/queries/blob_controls.query.graphql
+++ b/app/assets/javascripts/repository/queries/blob_controls.query.graphql
@@ -26,6 +26,9 @@ query getBlobControls($projectPath: ID!, $filePath: String!, $ref: String!, $ref
canCurrentUserPushToBranch
canModifyBlob
canModifyBlobWithWebIde
+ ideForkAndEditPath
+ forkAndEditPath
+ forkAndViewPath
simpleViewer {
__typename
fileType
diff --git a/app/assets/javascripts/repository/utils/fork_suggestion_utils.js b/app/assets/javascripts/repository/utils/fork_suggestion_utils.js
new file mode 100644
index 0000000000000000000000000000000000000000..2cbc76118f52c8a73bc322fc525cf0be73612328
--- /dev/null
+++ b/app/assets/javascripts/repository/utils/fork_suggestion_utils.js
@@ -0,0 +1,73 @@
+import { isLoggedIn } from '~/lib/utils/common_utils';
+import { createAlert, VARIANT_INFO } from '~/alert';
+import { __ } from '~/locale';
+
+export function showForkSuggestionAlert(forkAndViewPath) {
+ const i18n = {
+ forkSuggestion: __(
+ "You can't edit files directly in this project. Fork this project and submit a merge request with your changes.",
+ ),
+ fork: __('Fork'),
+ cancel: __('Cancel'),
+ };
+
+ return createAlert({
+ message: i18n.forkSuggestion,
+ variant: VARIANT_INFO,
+ primaryButton: {
+ text: i18n.fork,
+ link: forkAndViewPath,
+ },
+ secondaryButton: {
+ text: i18n.cancel,
+ clickHandler: (alert) => alert.dismiss(),
+ },
+ });
+}
+
+/**
+ * Checks if the user can fork the project
+ * @param {Object} userPermissions - User permissions object
+ * @param {boolean} isUsingLfs - Whether the project is using LFS
+ * @returns {boolean}
+ */
+export const canFork = (userPermissions, isUsingLfs) => {
+ const { createMergeRequestIn, forkProject } = userPermissions;
+ return isLoggedIn() && !isUsingLfs && createMergeRequestIn && forkProject;
+};
+
+/**
+ * Checks if the fork suggestion should be shown for single file editor
+ * @param {Object} userPermissions - User permissions object
+ * @param {boolean} isUsingLfs - Whether the project is using LFS
+ * @param {boolean} canModifyBlob - Whether the user can modify the blob
+ * @returns {boolean}
+ */
+export const showSingleFileEditorForkSuggestion = (userPermissions, isUsingLfs, canModifyBlob) => {
+ return canFork(userPermissions, isUsingLfs) && !canModifyBlob;
+};
+
+/**
+ * Checks if the fork suggestion should be shown for Web IDE
+ * @param {Object} userPermissions - User permissions object
+ * @param {boolean} isUsingLfs - Whether the project is using LFS
+ * @param {boolean} canModifyBlobWithWebIde - Whether the user can modify the blob with Web IDE
+ * @returns {boolean}
+ */
+export const showWebIdeForkSuggestion = (userPermissions, isUsingLfs, canModifyBlobWithWebIde) => {
+ return canFork(userPermissions, isUsingLfs) && !canModifyBlobWithWebIde;
+};
+
+/**
+ * Checks if the fork suggestion should be shown
+ * @param {Object} userPermissions - User permissions object
+ * @param {boolean} isUsingLfs - Whether the project is using LFS
+ * @param {Object} blobInfo - blobInfo object, including canModifyBlob and canModifyBlobWithWebIde
+ * @returns {boolean}
+ */
+export const showForkSuggestion = (userPermissions, isUsingLfs, blobInfo) => {
+ return (
+ showSingleFileEditorForkSuggestion(userPermissions, isUsingLfs, blobInfo.canModifyBlob) ||
+ showWebIdeForkSuggestion(userPermissions, isUsingLfs, blobInfo.canModifyBlobWithWebIde)
+ );
+};
diff --git a/ee/spec/frontend/repository/mock_data.js b/ee/spec/frontend/repository/mock_data.js
index 19af4775ffa5efb080cc03bfa3a421ecc88e9cc7..15380f309d255dd0a9ef4afff5c34b7fa77e3018 100644
--- a/ee/spec/frontend/repository/mock_data.js
+++ b/ee/spec/frontend/repository/mock_data.js
@@ -197,3 +197,58 @@ export const lockPathMutationMock = {
},
},
};
+
+export const blobControlsDataMock = {
+ __typename: 'Project',
+ id: '1234',
+ repository: {
+ __typename: 'Repository',
+ empty: false,
+ blobs: {
+ __typename: 'RepositoryBlobConnection',
+ nodes: [
+ {
+ __typename: 'RepositoryBlob',
+ id: '5678',
+ name: 'file.js',
+ blamePath: 'blame/file.js',
+ permalinkPath: 'permalink/file.js',
+ path: 'some/file.js',
+ storedExternally: false,
+ externalStorage: 'https://external-storage',
+ environmentFormattedExternalUrl: '',
+ environmentExternalUrlForRouteMap: '',
+ rawPath: 'https://testing.com/flightjs/flight/snippets/51/raw',
+ rawTextBlob: 'Example raw text content',
+ archived: false,
+ replacePath: 'some/replace/file.js',
+ webPath: 'some/file.js',
+ canCurrentUserPushToBranch: true,
+ canModifyBlob: true,
+ canModifyBlobWithWebIde: true,
+ ideForkAndEditPath: 'ide/fork/edit/path',
+ forkAndEditPath: 'fork/edit/path',
+ forkAndViewPath: 'fork/view/path',
+ simpleViewer: {
+ __typename: 'BlobViewer',
+ collapsed: false,
+ loadingPartialName: 'loading',
+ renderError: null,
+ tooLarge: false,
+ type: 'simple',
+ fileType: 'rich',
+ },
+ richViewer: {
+ __typename: 'BlobViewer',
+ collapsed: false,
+ loadingPartialName: 'loading',
+ renderError: 'too big file',
+ tooLarge: false,
+ type: 'rich',
+ fileType: 'rich',
+ },
+ },
+ ],
+ },
+ },
+};
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index c85a2c611175a7cba9a9e811722d7257f2cd947b..cffebf914d52ec6981ea87894eb11489779b3ce7 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -19162,6 +19162,9 @@ msgstr ""
msgid "Delete epic"
msgstr ""
+msgid "Delete file"
+msgstr ""
+
msgid "Delete group"
msgstr ""
@@ -47705,6 +47708,9 @@ msgstr ""
msgid "Replace all labels"
msgstr ""
+msgid "Replace file"
+msgstr ""
+
msgid "Replaced all labels with %{label_references} %{label_text}."
msgstr ""
@@ -66146,6 +66152,9 @@ msgstr ""
msgid "You can't approve because you added one or more commits to this merge request."
msgstr ""
+msgid "You can't edit files directly in this project. Fork this project and submit a merge request with your changes."
+msgstr ""
+
msgid "You can't follow more than %{limit} users. To follow more users, unfollow some others."
msgstr ""
diff --git a/spec/frontend/repository/components/header_area/blob_button_group_spec.js b/spec/frontend/repository/components/header_area/blob_button_group_spec.js
index 374b55f0b536706f3926fc1e7c031b0099eb7100..8d703e4ec56f46845906384a06df6df9b77641bb 100644
--- a/spec/frontend/repository/components/header_area/blob_button_group_spec.js
+++ b/spec/frontend/repository/components/header_area/blob_button_group_spec.js
@@ -1,7 +1,7 @@
import { GlDisclosureDropdownItem } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import { projectMock } from 'ee_else_ce_jest/repository/mock_data';
+import { projectMock, blobControlsDataMock } from 'ee_else_ce_jest/repository/mock_data';
import projectInfoQuery from 'ee_else_ce/repository/queries/project_info.query.graphql';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { stubComponent } from 'helpers/stub_component';
@@ -19,11 +19,6 @@ jest.mock('~/lib/utils/common_utils', () => ({
}));
const DEFAULT_PROPS = {
- name: 'some name',
- path: 'some/path',
- replacePath: 'some/replace/path',
- deletePath: 'some/delete/path',
- canPushToBranch: true,
isEmptyRepository: false,
projectPath: 'some/project/path',
isUsingLfs: true,
@@ -32,8 +27,7 @@ const DEFAULT_PROPS = {
const DEFAULT_INJECT = {
targetBranch: 'master',
originalBranch: 'master',
- canModifyBlob: true,
- canModifyBlobWithWebIde: true,
+ blobInfo: blobControlsDataMock.repository.blobs.nodes[0],
};
describe('BlobButtonGroup component', () => {
@@ -105,8 +99,9 @@ describe('BlobButtonGroup component', () => {
it('renders component', () => {
expect(wrapper.props()).toMatchObject({
- name: 'some name',
- path: 'some/path',
+ isEmptyRepository: false,
+ isUsingLfs: true,
+ projectPath: 'some/project/path',
});
});
@@ -141,48 +136,70 @@ describe('BlobButtonGroup component', () => {
beforeEach(async () => {
await createComponent({
props: { isUsingLfs: false },
- inject: { canModifyBlob: false, canModifyBlobWithWebIde: false },
+ inject: {
+ blobInfo: {
+ ...blobControlsDataMock.repository.blobs.nodes[0],
+ canModifyBlob: false,
+ canModifyBlobWithWebIde: false,
+ },
+ },
});
});
it('does not trigger the UploadBlobModal from the replace item', () => {
findReplaceItem().vm.$emit('action');
- expect(findReplaceItem().props('item')).toMatchObject({
- extraAttrs: { disabled: true },
- });
-
expect(showUploadBlobModalMock).not.toHaveBeenCalled();
- expect(wrapper.emitted().fork).toHaveLength(1);
+ expect(createAlert).toHaveBeenCalledWith({
+ message:
+ 'You can’t edit files directly in this project. Fork this project and submit a merge request with your changes.',
+ primaryButton: {
+ link: 'fork/view/path',
+ text: 'Fork',
+ },
+ secondaryButton: {
+ clickHandler: expect.any(Function),
+ text: 'Cancel',
+ },
+ variant: 'info',
+ });
});
it('does not trigger the DeleteBlobModal from the delete item', () => {
findDeleteItem().vm.$emit('action');
- expect(findDeleteItem().props('item')).toMatchObject({
- extraAttrs: { disabled: true },
- });
-
expect(showDeleteBlobModalMock).not.toHaveBeenCalled();
- expect(wrapper.emitted().fork).toHaveLength(1);
+ expect(createAlert).toHaveBeenCalledWith({
+ message:
+ 'You can’t edit files directly in this project. Fork this project and submit a merge request with your changes.',
+ primaryButton: {
+ link: 'fork/view/path',
+ text: 'Fork',
+ },
+ secondaryButton: {
+ clickHandler: expect.any(Function),
+ text: 'Cancel',
+ },
+ variant: 'info',
+ });
});
});
});
it('renders UploadBlobModal', () => {
expect(findUploadBlobModal().props()).toMatchObject({
- commitMessage: 'Replace some name',
+ commitMessage: 'Replace file.js',
targetBranch: 'master',
originalBranch: 'master',
canPushCode: true,
- path: 'some/path',
- replacePath: 'some/replace/path',
+ path: 'some/file.js',
+ replacePath: 'some/replace/file.js',
});
});
it('renders CommitChangesModal for delete', () => {
expect(findDeleteBlobModal().props()).toMatchObject({
- commitMessage: 'Delete some name',
+ commitMessage: 'Delete file.js',
targetBranch: 'master',
originalBranch: 'master',
canPushCode: true,
diff --git a/spec/frontend/repository/components/header_area/blob_controls_spec.js b/spec/frontend/repository/components/header_area/blob_controls_spec.js
index ad98ce073cbb6847794c1f7da1e4243c42f8d2a1..14539c594fd8780c84825cc5f4b9db3d335fa675 100644
--- a/spec/frontend/repository/components/header_area/blob_controls_spec.js
+++ b/spec/frontend/repository/components/header_area/blob_controls_spec.js
@@ -166,33 +166,10 @@ describe('Blob controls component', () => {
expect(findOverflowMenu().exists()).toBe(true);
expect(findOverflowMenu().props()).toEqual({
- name: 'file.js',
projectPath: 'some/project',
- path: 'some/file.js',
- rawPath: 'https://testing.com/flightjs/flight/snippets/51/raw',
isBinary: true,
- environmentName: '',
- environmentPath: '',
isEmpty: false,
overrideCopy: true,
- archived: false,
- replacePath: 'some/replace/file.js',
- webPath: 'some/file.js',
- canCurrentUserPushToBranch: true,
- simpleViewer: {
- __typename: 'BlobViewer',
- renderError: null,
- tooLarge: false,
- type: 'simple',
- fileType: 'rich',
- },
- richViewer: {
- __typename: 'BlobViewer',
- renderError: 'too big file',
- tooLarge: false,
- type: 'rich',
- fileType: 'rich',
- },
isEmptyRepository: false,
isUsingLfs: false,
});
diff --git a/spec/frontend/repository/components/header_area/blob_default_actions_group_spec.js b/spec/frontend/repository/components/header_area/blob_default_actions_group_spec.js
index 54930a587a9cf909267d43b553304a1e94b8c59c..a0ab8e8e8183f8cf3516295e57792659c4accae9 100644
--- a/spec/frontend/repository/components/header_area/blob_default_actions_group_spec.js
+++ b/spec/frontend/repository/components/header_area/blob_default_actions_group_spec.js
@@ -1,10 +1,12 @@
import { shallowMount } from '@vue/test-utils';
import { GlDisclosureDropdownItem } from '@gitlab/ui';
import BlobDefaultActionsGroup from '~/repository/components/header_area/blob_default_actions_group.vue';
+import { blobControlsDataMock } from '../../mock_data';
const mockBlobHash = 'foo-bar';
const mockEnvironmentName = 'my.testing.environment';
const mockEnvironmentPath = 'https://my.testing.environment';
+const blobInfoMock = blobControlsDataMock.repository.blobs.nodes[0];
describe('Blob Default Actions Group', () => {
let wrapper;
@@ -12,9 +14,6 @@ describe('Blob Default Actions Group', () => {
const createComponent = (props = {}, provide = {}) => {
wrapper = shallowMount(BlobDefaultActionsGroup, {
propsData: {
- name: 'dummy.md',
- path: 'foo/bar/dummy.md',
- rawPath: 'https://testing.com/flightjs/flight/snippets/51/raw',
blobHash: mockBlobHash,
activeViewerType: 'simple',
hasRenderError: false,
@@ -27,6 +26,10 @@ describe('Blob Default Actions Group', () => {
provide: {
blobHash: mockBlobHash,
canDownloadCode: true,
+ blobInfo: {
+ ...blobInfoMock,
+ ...provide.blobInfo,
+ },
...provide,
},
});
@@ -38,7 +41,8 @@ describe('Blob Default Actions Group', () => {
const findCopyFileContentItem = () => findDropdownItemWithText('Copy file contents');
const findViewRawItem = () => findDropdownItemWithText('Open raw');
const findDownloadItem = () => findDropdownItemWithText('Download');
- const findEnvironmentItem = () => findDropdownItemWithText(`View on ${mockEnvironmentName}`);
+ const findEnvironmentItem = () =>
+ findDropdownItemWithText(`View on ${blobInfoMock.environmentFormattedExternalUrl}`);
beforeEach(() => {
createComponent();
@@ -106,7 +110,16 @@ describe('Blob Default Actions Group', () => {
'when environmentName is $environmentName and environmentPath is $environmentPath',
({ environmentName, environmentPath, isVisible }) => {
it(`${isVisible ? 'renders' : 'does not render'} the button`, () => {
- createComponent({ environmentName, environmentPath });
+ createComponent(
+ {},
+ {
+ blobInfo: {
+ ...blobInfoMock,
+ environmentFormattedExternalUrl: environmentName,
+ environmentExternalUrlForRouteMap: environmentPath,
+ },
+ },
+ );
expect(findEnvironmentItem()).toEqual(isVisible);
});
@@ -114,10 +127,15 @@ describe('Blob Default Actions Group', () => {
);
it('renders the correct props', () => {
- createComponent({
- environmentName: mockEnvironmentName,
- environmentPath: mockEnvironmentPath,
- });
+ createComponent(
+ {},
+ {
+ blobInfo: {
+ environmentFormattedExternalUrl: mockEnvironmentName,
+ environmentExternalUrlForRouteMap: mockEnvironmentPath,
+ },
+ },
+ );
expect(findEnvironmentItem().props('item')).toMatchObject({
text: `View on ${mockEnvironmentName}`,
diff --git a/spec/frontend/repository/components/header_area/blob_overflow_menu_spec.js b/spec/frontend/repository/components/header_area/blob_overflow_menu_spec.js
index 6a36820bd735cf26fc8ac30cff3ca511dfef7000..d3d207525b4ffc1721d64aed8ac43c67f1a0f765 100644
--- a/spec/frontend/repository/components/header_area/blob_overflow_menu_spec.js
+++ b/spec/frontend/repository/components/header_area/blob_overflow_menu_spec.js
@@ -23,23 +23,11 @@ describe('Blob Overflow Menu', () => {
wrapper = shallowMountExtended(BlobOverflowMenu, {
router,
provide: {
- canModifyBlob: true,
- canModifyBlobWithWebIde: true,
+ blobInfo: blobControlsDataMock.repository.blobs.nodes[0],
...provided,
},
propsData: {
- path: blobControlsDataMock.repository.blobs.nodes[0].path,
- rawPath: blobControlsDataMock.repository.blobs.nodes[0].rawPath,
projectPath,
- richViewer: blobControlsDataMock.repository.blobs.nodes[0].richViewer,
- simpleViewer: blobControlsDataMock.repository.blobs.nodes[0].simpleViewer,
- name: blobControlsDataMock.repository.blobs.nodes[0].name,
- isBinary: blobControlsDataMock.repository.blobs.nodes[0].binary,
- archived: blobControlsDataMock.repository.blobs.nodes[0].archived,
- replacePath: blobControlsDataMock.repository.blobs.nodes[0].replacePath,
- webPath: blobControlsDataMock.repository.blobs.nodes[0].webPath,
- canCurrentUserPushToBranch:
- blobControlsDataMock.repository.blobs.nodes[0].canCurrentUserPushToBranch,
...propsData,
},
stub: {
@@ -87,9 +75,15 @@ describe('Blob Overflow Menu', () => {
});
it('does not render when blob is archived', () => {
- createComponent({
- archived: true,
- });
+ createComponent(
+ {},
+ {
+ blobInfo: {
+ ...blobControlsDataMock.repository.blobs.nodes[0],
+ archived: true,
+ },
+ },
+ );
expect(findBlobButtonGroup().exists()).toBe(false);
});
diff --git a/spec/frontend/repository/mock_data.js b/spec/frontend/repository/mock_data.js
index 585c59ec34e37e90d3a1d7998b030dd75e2e9716..dbac72845bd29a722a4ce886b13144896c7258e4 100644
--- a/spec/frontend/repository/mock_data.js
+++ b/spec/frontend/repository/mock_data.js
@@ -106,8 +106,8 @@ export const blobControlsDataMock = {
path: 'some/file.js',
storedExternally: false,
externalStorage: 'https://external-storage',
- environmentFormattedExternalUrl: '',
- environmentExternalUrlForRouteMap: '',
+ environmentFormattedExternalUrl: 'my.testing.environment',
+ environmentExternalUrlForRouteMap: 'https://my.testing.environment',
rawPath: 'https://testing.com/flightjs/flight/snippets/51/raw',
rawTextBlob: 'Example raw text content',
archived: false,
@@ -116,6 +116,9 @@ export const blobControlsDataMock = {
canCurrentUserPushToBranch: true,
canModifyBlob: true,
canModifyBlobWithWebIde: true,
+ ideForkAndEditPath: 'ide/fork/edit/path',
+ forkAndEditPath: 'fork/edit/path',
+ forkAndViewPath: 'fork/view/path',
simpleViewer: {
__typename: 'BlobViewer',
collapsed: false,
diff --git a/spec/frontend/repository/utils/fork_suggestion_utils_spec.js b/spec/frontend/repository/utils/fork_suggestion_utils_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..d111f65e078e683f7cf24a67ba314ddd913f839c
--- /dev/null
+++ b/spec/frontend/repository/utils/fork_suggestion_utils_spec.js
@@ -0,0 +1,148 @@
+import { createAlert, VARIANT_INFO } from '~/alert';
+import * as commonUtils from '~/lib/utils/common_utils';
+import {
+ showForkSuggestionAlert,
+ canFork,
+ showSingleFileEditorForkSuggestion,
+ showWebIdeForkSuggestion,
+ showForkSuggestion,
+} from '~/repository/utils/fork_suggestion_utils';
+
+jest.mock('~/alert');
+jest.mock('~/lib/utils/common_utils');
+
+describe('forkSuggestionUtils', () => {
+ let userPermissions;
+ const createUserPermissions = (createMergeRequestIn = true, forkProject = true) => ({
+ createMergeRequestIn,
+ forkProject,
+ });
+
+ beforeEach(() => {
+ commonUtils.isLoggedIn.mockReturnValue(true);
+ userPermissions = createUserPermissions();
+ });
+
+ describe('canFork', () => {
+ it('returns true when all conditions are met', () => {
+ expect(canFork(userPermissions, false)).toBe(true);
+ });
+
+ it('returns false when user is not logged in', () => {
+ commonUtils.isLoggedIn.mockReturnValue(false);
+ expect(canFork(userPermissions, false)).toBe(false);
+ });
+
+ it('returns false when project is using LFS', () => {
+ expect(canFork(userPermissions, true)).toBe(false);
+ });
+
+ it('returns false when user cannot create merge request', () => {
+ userPermissions = createUserPermissions(false, true);
+ expect(canFork(userPermissions, false)).toBe(false);
+ });
+
+ it('returns false when user cannot fork project', () => {
+ userPermissions = createUserPermissions(true, false);
+ expect(canFork(userPermissions, false)).toBe(false);
+ });
+ });
+
+ describe('showSingleFileEditorForkSuggestion', () => {
+ it('returns true when user can fork but cannot modify blob', () => {
+ expect(showSingleFileEditorForkSuggestion(userPermissions, false, false)).toBe(true);
+ });
+
+ it('returns false when user can fork and can modify blob', () => {
+ expect(showSingleFileEditorForkSuggestion(userPermissions, false, true)).toBe(false);
+ });
+ });
+
+ describe('showWebIdeForkSuggestion', () => {
+ it('returns true when user can fork but cannot modify blob with Web IDE', () => {
+ expect(showWebIdeForkSuggestion(userPermissions, false, false)).toBe(true);
+ });
+
+ it('returns false when user can fork and can modify blob with Web IDE', () => {
+ expect(showWebIdeForkSuggestion(userPermissions, false, true)).toBe(false);
+ });
+ });
+
+ describe('showForkSuggestion', () => {
+ it('returns true when single file editor fork suggestion is true', () => {
+ expect(
+ showForkSuggestion(userPermissions, false, {
+ canModifyBlob: false,
+ canModifyBlobWithWebIde: true,
+ }),
+ ).toBe(true);
+ });
+
+ it('returns true when Web IDE fork suggestion is true', () => {
+ expect(
+ showForkSuggestion(userPermissions, false, {
+ canModifyBlob: true,
+ canModifyBlobWithWebIde: false,
+ }),
+ ).toBe(true);
+ });
+
+ it('returns false when both fork suggestions are false', () => {
+ expect(
+ showForkSuggestion(userPermissions, false, {
+ canModifyBlob: true,
+ canModifyBlobWithWebIde: true,
+ }),
+ ).toBe(false);
+ });
+
+ it('returns false when user cannot fork', () => {
+ commonUtils.isLoggedIn.mockReturnValue(false);
+ expect(
+ showForkSuggestion(userPermissions, false, {
+ canModifyBlob: false,
+ canModifyBlobWithWebIde: false,
+ }),
+ ).toBe(false);
+ });
+ });
+
+ describe('showForkSuggestionAlert', () => {
+ const forkAndViewPath = '/path/to/fork';
+
+ beforeEach(() => {
+ createAlert.mockClear();
+ });
+
+ it('calls createAlert with correct parameters', () => {
+ showForkSuggestionAlert(forkAndViewPath);
+
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledWith({
+ message:
+ "You can't edit files directly in this project. Fork this project and submit a merge request with your changes.",
+ variant: VARIANT_INFO,
+ primaryButton: {
+ text: 'Fork',
+ link: forkAndViewPath,
+ },
+ secondaryButton: {
+ text: 'Cancel',
+ clickHandler: expect.any(Function),
+ },
+ });
+ });
+
+ it('secondary button click handler dismisses the alert', () => {
+ const mockAlert = { dismiss: jest.fn() };
+ createAlert.mockReturnValue(mockAlert);
+
+ showForkSuggestionAlert(forkAndViewPath);
+
+ const secondaryButtonClickHandler = createAlert.mock.calls[0][0].secondaryButton.clickHandler;
+ secondaryButtonClickHandler(mockAlert);
+
+ expect(mockAlert.dismiss).toHaveBeenCalledTimes(1);
+ });
+ });
+});