Skip to content
Snippets Groups Projects
Commit 28943cbd authored by Vasilii Iakliushin's avatar Vasilii Iakliushin :two:
Browse files

Merge branch '392016-add-group-readme-in-group-settings' into 'master'

Group Settings - Add Group README

See merge request !115604



Merged-by: Vasilii Iakliushin's avatarVasilii Iakliushin <viakliushin@gitlab.com>
Approved-by: default avatarRudy Crespo <rcrespo@gitlab.com>
Approved-by: Alex Buijs's avatarAlex Buijs <abuijs@gitlab.com>
Approved-by: default avatarBecka Lippert <rlippert@gitlab.com>
Approved-by: Vasilii Iakliushin's avatarVasilii Iakliushin <viakliushin@gitlab.com>
Reviewed-by: Vasilii Iakliushin's avatarVasilii Iakliushin <viakliushin@gitlab.com>
Reviewed-by: default avatarVitaly Slobodin <vslobodin@gitlab.com>
Co-authored-by: Zack Cuddy's avatarZachary Cuddy <zcuddy@gitlab.com>
parents 515e9bc0 a1c96b23
No related branches found
No related tags found
2 merge requests!118700Remove refactor_vulnerability_filters feature flag,!115604Group Settings - Add Group README
Pipeline #833816645 passed
Showing
with 524 additions and 0 deletions
......@@ -35,6 +35,13 @@ export function getProjects(query, options, callback = () => {}) {
});
}
export function createProject(projectData) {
const url = buildApiUrl(PROJECTS_PATH);
return axios.post(url, projectData).then(({ data }) => {
return data;
});
}
export function importProjectMembers(sourceId, targetId) {
const url = buildApiUrl(PROJECT_IMPORT_MEMBERS_PATH)
.replace(':id', sourceId)
......
<script>
import { GlButton, GlModal, GlModalDirective, GlSprintf } from '@gitlab/ui';
import { s__, __ } from '~/locale';
import { createProject } from '~/rest_api';
import { createAlert } from '~/alert';
import { openWebIDE } from '~/lib/utils/web_ide_navigator';
import { README_MODAL_ID, GITLAB_README_PROJECT, README_FILE } from '../constants';
export default {
name: 'GroupSettingsReadme',
i18n: {
readme: __('README'),
addReadme: __('Add README'),
cancel: __('Cancel'),
createProjectAndReadme: s__('Groups|Create and add README'),
creatingReadme: s__('Groups|Creating README'),
existingProjectNewReadme: s__('Groups|This will create a README.md for project %{path}.'),
newProjectAndReadme: s__('Groups|This will create a project %{path} and add a README.md.'),
errorCreatingProject: s__('Groups|There was an error creating the Group README.'),
},
components: {
GlButton,
GlModal,
GlSprintf,
},
directives: {
GlModal: GlModalDirective,
},
props: {
groupReadmePath: {
type: String,
required: false,
default: '',
},
readmeProjectPath: {
type: String,
required: false,
default: '',
},
groupPath: {
type: String,
required: true,
},
groupId: {
type: String,
required: true,
},
},
data() {
return {
creatingReadme: false,
};
},
computed: {
hasReadme() {
return this.groupReadmePath.length > 0;
},
hasReadmeProject() {
return this.readmeProjectPath.length > 0;
},
pathToReadmeProject() {
return this.hasReadmeProject
? this.readmeProjectPath
: `${this.groupPath}/${GITLAB_README_PROJECT}`;
},
modalBody() {
return this.hasReadmeProject
? this.$options.i18n.existingProjectNewReadme
: this.$options.i18n.newProjectAndReadme;
},
modalSubmitButtonText() {
return this.hasReadmeProject
? this.$options.i18n.addReadme
: this.$options.i18n.createProjectAndReadme;
},
},
methods: {
hideModal() {
this.$refs.modal.hide();
},
createReadme() {
if (this.hasReadmeProject) {
openWebIDE(this.readmeProjectPath, README_FILE);
} else {
this.createProjectWithReadme();
}
},
createProjectWithReadme() {
this.creatingReadme = true;
const projectData = {
name: GITLAB_README_PROJECT,
namespace_id: this.groupId,
};
createProject(projectData)
.then(({ path_with_namespace: pathWithNamespace }) => {
openWebIDE(pathWithNamespace, README_FILE);
})
.catch(() => {
this.hideModal();
this.creatingReadme = false;
createAlert({ message: this.$options.i18n.errorCreatingProject });
});
},
},
README_MODAL_ID,
};
</script>
<template>
<div>
<gl-button v-if="hasReadme" icon="doc-text" :href="groupReadmePath">{{
$options.i18n.readme
}}</gl-button>
<gl-button
v-else
v-gl-modal="$options.README_MODAL_ID"
variant="dashed"
icon="file-addition"
data-testid="group-settings-add-readme-button"
>{{ $options.i18n.addReadme }}</gl-button
>
<gl-modal ref="modal" :modal-id="$options.README_MODAL_ID" :title="$options.i18n.addReadme">
<div data-testid="group-settings-modal-readme-body">
<gl-sprintf :message="modalBody">
<template #path>
<code>{{ pathToReadmeProject }}</code>
</template>
</gl-sprintf>
</div>
<template #modal-footer>
<gl-button variant="default" @click="hideModal">{{ $options.i18n.cancel }}</gl-button>
<gl-button v-if="creatingReadme" variant="default" loading disabled>{{
$options.i18n.creatingReadme
}}</gl-button>
<gl-button
v-else
variant="confirm"
data-testid="group-settings-modal-create-readme-button"
@click="createReadme"
>{{ modalSubmitButtonText }}</gl-button
>
</template>
</gl-modal>
</div>
</template>
export const LEVEL_TYPES = {
GROUP: 'group',
};
export const README_MODAL_ID = 'add_group_readme_modal';
export const GITLAB_README_PROJECT = 'gitlab-profile';
export const README_FILE = 'README.md';
import Vue from 'vue';
import GroupSettingsReadme from './components/group_settings_readme.vue';
export const initGroupSettingsReadme = () => {
const el = document.getElementById('js-group-settings-readme');
if (!el) return false;
const { groupReadmePath, readmeProjectPath, groupPath, groupId } = el.dataset;
return new Vue({
el,
render(createElement) {
return createElement(GroupSettingsReadme, {
props: {
groupReadmePath,
readmeProjectPath,
groupPath,
groupId,
},
});
},
});
};
import { visitUrl, webIDEUrl } from '~/lib/utils/url_utility';
/**
* Takes a project path and optional file path and branch
* and then redirects the user to the web IDE.
*
* @param {string} projectPath - Full path to project including namespace (ex. flightjs/Flight)
* @param {string} filePath - optional path to file to be edited, otherwise will open at base directory (ex. README.md)
* @param {string} branch - optional branch to open the IDE, defaults to 'main'
*/
export const openWebIDE = (projectPath, filePath, branch = 'main') => {
if (!projectPath) {
throw new TypeError('projectPath parameter is required');
}
const pathnameSegments = [projectPath, 'edit', branch, '-'];
if (filePath) {
pathnameSegments.push(filePath);
}
visitUrl(webIDEUrl(`/${pathnameSegments.join('/')}/`));
};
......@@ -9,6 +9,7 @@ import mountBadgeSettings from '~/pages/shared/mount_badge_settings';
import initSearchSettings from '~/search_settings';
import initSettingsPanels from '~/settings_panels';
import initConfirmDanger from '~/init_confirm_danger';
import { initGroupSettingsReadme } from '~/groups/settings/init_group_settings_readme';
initFilePickers();
initConfirmDanger();
......@@ -27,3 +28,5 @@ initProjectSelects();
initSearchSettings();
initCascadingSettingsLockPopovers();
initGroupSettingsReadme();
......@@ -180,6 +180,15 @@ def show_group_readme?(group)
Feature.enabled?(:show_group_readme, group) && group.group_readme
end
def group_settings_readme_app_data(group)
{
group_readme_path: group.group_readme&.present&.web_path,
readme_project_path: group.readme_project&.present&.path_with_namespace,
group_path: group.full_path,
group_id: group.id
}
end
def enabled_git_access_protocol_options_for_group
case ::Gitlab::CurrentSettings.enabled_git_access_protocol
when nil, ""
......
......@@ -19,6 +19,12 @@
= f.label :description, s_('Groups|Group description (optional)'), class: 'label-bold'
= f.text_area :description, class: 'form-control', rows: 3, maxlength: 250
- if Feature.enabled?(:show_group_readme, @group)
.row.gl-mt-3
.form-group.col-md-5
= f.label :description, s_('Groups|Group README'), class: 'label-bold'
#js-group-settings-readme{ data: group_settings_readme_app_data(@group) }
= render 'shared/repository_size_limit_setting_registration_features_cta', form: f
= render_if_exists 'shared/repository_size_limit_setting', form: f, type: :group
......
......@@ -21042,12 +21042,21 @@ msgstr ""
msgid "Groups|Checking group URL availability..."
msgstr ""
 
msgid "Groups|Create and add README"
msgstr ""
msgid "Groups|Creating README"
msgstr ""
msgid "Groups|Enter a descriptive name for your group."
msgstr ""
 
msgid "Groups|Group ID"
msgstr ""
 
msgid "Groups|Group README"
msgstr ""
msgid "Groups|Group URL"
msgstr ""
 
......@@ -21093,6 +21102,15 @@ msgstr ""
msgid "Groups|Subgroup slug"
msgstr ""
 
msgid "Groups|There was an error creating the Group README."
msgstr ""
msgid "Groups|This will create a README.md for project %{path}."
msgstr ""
msgid "Groups|This will create a project %{path} and add a README.md."
msgstr ""
msgid "Groups|You're creating a new top-level group"
msgstr ""
 
......@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'Edit group settings', feature_category: :subgroups do
include Spec::Support::Helpers::ModalHelpers
let(:user) { create(:user) }
let(:group) { create(:group, path: 'foo') }
......@@ -244,6 +246,77 @@
end
end
describe 'group README', :js do
let_it_be(:group) { create(:group) }
context 'with gitlab-profile project and README.md' do
let_it_be(:project) { create(:project, :readme, namespace: group) }
it 'renders link to Group README and navigates to it on click' do
visit edit_group_path(group)
wait_for_requests
click_link('README')
wait_for_requests
expect(page).to have_current_path(project_blob_path(project, "#{project.default_branch}/README.md"))
expect(page).to have_text('README.md')
end
end
context 'with gitlab-profile project and no README.md' do
let_it_be(:project) { create(:project, name: 'gitlab-profile', namespace: group) }
it 'renders Add README button and allows user to create a README via the IDE' do
visit edit_group_path(group)
wait_for_requests
expect(page).not_to have_selector('.ide')
click_button('Add README')
accept_gl_confirm("This will create a README.md for project #{group.readme_project.present.path_with_namespace}.", button_text: 'Add README')
wait_for_requests
expect(page).to have_current_path("/-/ide/project/#{group.readme_project.present.path_with_namespace}/edit/main/-/README.md/")
page.within('.ide') do
expect(page).to have_text('README.md')
end
end
end
context 'with no gitlab-profile project and no README.md' do
it 'renders Add README button and allows user to create both the gitlab-profile project and README via the IDE' do
visit edit_group_path(group)
wait_for_requests
expect(page).not_to have_selector('.ide')
click_button('Add README')
accept_gl_confirm("This will create a project #{group.full_path}/gitlab-profile and add a README.md.", button_text: 'Create and add README')
wait_for_requests
expect(page).to have_current_path("/-/ide/project/#{group.full_path}/gitlab-profile/edit/main/-/README.md/")
page.within('.ide') do
expect(page).to have_text('README.md')
end
end
end
describe 'with :show_group_readme FF false' do
before do
stub_feature_flags(show_group_readme: false)
end
it 'does not render Group README settings' do
expect(page).not_to have_text('README')
end
end
end
def update_path(new_group_path)
visit edit_group_path(group)
......
......@@ -67,6 +67,20 @@ describe('~/api/projects_api.js', () => {
});
});
describe('createProject', () => {
it('posts to the correct URL and returns the data', () => {
const body = { name: 'test project' };
const expectedUrl = '/api/v7/projects.json';
const expectedRes = { id: 999, name: 'test project' };
mock.onPost(expectedUrl, body).replyOnce(HTTP_STATUS_OK, { data: expectedRes });
return projectsApi.createProject(body).then(({ data }) => {
expect(data).toStrictEqual(expectedRes);
});
});
});
describe('importProjectMembers', () => {
beforeEach(() => {
jest.spyOn(axios, 'post');
......
import { GlModal, GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import GroupSettingsReadme from '~/groups/settings/components/group_settings_readme.vue';
import { GITLAB_README_PROJECT } from '~/groups/settings/constants';
import {
MOCK_GROUP_PATH,
MOCK_GROUP_ID,
MOCK_PATH_TO_GROUP_README,
MOCK_PATH_TO_README_PROJECT,
} from '../mock_data';
describe('GroupSettingsReadme', () => {
let wrapper;
const defaultProps = {
groupPath: MOCK_GROUP_PATH,
groupId: MOCK_GROUP_ID,
};
const createComponent = (props = {}) => {
wrapper = shallowMountExtended(GroupSettingsReadme, {
propsData: {
...defaultProps,
...props,
},
stubs: {
GlModal,
GlSprintf,
},
});
};
const findHasReadmeButtonLink = () => wrapper.findByText('README');
const findAddReadmeButton = () => wrapper.findByTestId('group-settings-add-readme-button');
const findModalBody = () => wrapper.findByTestId('group-settings-modal-readme-body');
const findModalCreateReadmeButton = () =>
wrapper.findByTestId('group-settings-modal-create-readme-button');
describe('Group has existing README', () => {
beforeEach(() => {
createComponent({
groupReadmePath: MOCK_PATH_TO_GROUP_README,
readmeProjectPath: MOCK_PATH_TO_README_PROJECT,
});
});
describe('template', () => {
it('renders README Button Link with correct path and text', () => {
expect(findHasReadmeButtonLink().exists()).toBe(true);
expect(findHasReadmeButtonLink().attributes('href')).toBe(MOCK_PATH_TO_GROUP_README);
});
it('does not render Add README Button', () => {
expect(findAddReadmeButton().exists()).toBe(false);
});
});
});
describe('Group has README project without README file', () => {
beforeEach(() => {
createComponent({ readmeProjectPath: MOCK_PATH_TO_README_PROJECT });
});
describe('template', () => {
it('does not render README', () => {
expect(findHasReadmeButtonLink().exists()).toBe(false);
});
it('does render Add Readme Button with correct text', () => {
expect(findAddReadmeButton().exists()).toBe(true);
expect(findAddReadmeButton().text()).toBe('Add README');
});
it('generates a hidden modal with correct body text', () => {
expect(findModalBody().text()).toMatchInterpolatedText(
`This will create a README.md for project ${MOCK_PATH_TO_README_PROJECT}.`,
);
});
it('generates a hidden modal with correct button text', () => {
expect(findModalCreateReadmeButton().text()).toBe('Add README');
});
});
});
describe('Group does not have README project', () => {
beforeEach(() => {
createComponent();
});
describe('template', () => {
it('does not render README', () => {
expect(findHasReadmeButtonLink().exists()).toBe(false);
});
it('does render Add Readme Button with correct text', () => {
expect(findAddReadmeButton().exists()).toBe(true);
expect(findAddReadmeButton().text()).toBe('Add README');
});
it('generates a hidden modal with correct body text', () => {
expect(findModalBody().text()).toMatchInterpolatedText(
`This will create a project ${MOCK_GROUP_PATH}/${GITLAB_README_PROJECT} and add a README.md.`,
);
});
it('generates a hidden modal with correct button text', () => {
expect(findModalCreateReadmeButton().text()).toBe('Create and add README');
});
});
});
});
export const MOCK_GROUP_PATH = 'test-group';
export const MOCK_GROUP_ID = '999';
export const MOCK_PATH_TO_GROUP_README = '/group/project/-/blob/main/README.md';
export const MOCK_PATH_TO_README_PROJECT = 'group/project';
import { visitUrl, webIDEUrl } from '~/lib/utils/url_utility';
import { openWebIDE } from '~/lib/utils/web_ide_navigator';
jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn(),
webIDEUrl: jest.fn().mockImplementation((path) => `/-/ide/projects${path}`),
}));
describe('openWebIDE', () => {
it('when called without projectPath throws TypeError and does not call visitUrl', () => {
expect(() => {
openWebIDE();
}).toThrow(new TypeError('projectPath parameter is required'));
expect(visitUrl).not.toHaveBeenCalled();
});
it('when called with projectPath and without fileName calls visitUrl with correct path', () => {
const params = { projectPath: 'project-path' };
const expectedNonIDEPath = `/${params.projectPath}/edit/main/-/`;
const expectedIDEPath = `/-/ide/projects${expectedNonIDEPath}`;
openWebIDE(params.projectPath);
expect(webIDEUrl).toHaveBeenCalledWith(expectedNonIDEPath);
expect(visitUrl).toHaveBeenCalledWith(expectedIDEPath);
});
it('when called with projectPath and fileName calls visitUrl with correct path', () => {
const params = { projectPath: 'project-path', fileName: 'README' };
const expectedNonIDEPath = `/${params.projectPath}/edit/main/-/${params.fileName}/`;
const expectedIDEPath = `/-/ide/projects${expectedNonIDEPath}`;
openWebIDE(params.projectPath, params.fileName);
expect(webIDEUrl).toHaveBeenCalledWith(expectedNonIDEPath);
expect(visitUrl).toHaveBeenCalledWith(expectedIDEPath);
});
});
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'groups/settings/_general.html.haml', feature_category: :subgroups do
describe 'Group Settings README' do
let_it_be(:group) { build_stubbed(:group) }
let_it_be(:user) { build_stubbed(:admin) }
before do
assign(:group, group)
allow(view).to receive(:current_user).and_return(user)
end
describe 'with :show_group_readme FF true' do
before do
stub_feature_flags(show_group_readme: true)
end
it 'renders #js-group-settings-readme' do
render
expect(rendered).to have_selector('#js-group-settings-readme')
end
end
describe 'with :show_group_readme FF false' do
before do
stub_feature_flags(show_group_readme: false)
end
it 'does not render #js-group-settings-readme' do
render
expect(rendered).not_to have_selector('#js-group-settings-readme')
end
end
end
end
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment