Commit e0c1d02d authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '267488-clean-up-the-create-project-ui-experiment' into 'master'

Clean up the Create Project UI experiment

See merge request gitlab-org/gitlab!59452
parents 841b162d ead6cd1a
<script>
/* eslint-disable vue/no-v-html */
import { GlBreadcrumb, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import createFromTemplateIllustration from '@gitlab/svgs/dist/illustrations/project-create-from-template-sm.svg';
import blankProjectIllustration from '@gitlab/svgs/dist/illustrations/project-create-new-sm.svg';
import importProjectIllustration from '@gitlab/svgs/dist/illustrations/project-import-sm.svg';
import ciCdProjectIllustration from '@gitlab/svgs/dist/illustrations/project-run-CICD-pipelines-sm.svg';
import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { experiment } from '~/experimentation/utils';
import { __, s__ } from '~/locale';
import { NEW_REPO_EXPERIMENT } from '../constants';
import blankProjectIllustration from '../illustrations/blank-project.svg';
import ciCdProjectIllustration from '../illustrations/ci-cd-project.svg';
import createFromTemplateIllustration from '../illustrations/create-from-template.svg';
import importProjectIllustration from '../illustrations/import-project.svg';
import LegacyContainer from './legacy_container.vue';
import WelcomePage from './welcome.vue';
import { s__ } from '~/locale';
import NewNamespacePage from '~/vue_shared/new_namespace/new_namespace_page.vue';
import NewProjectPushTipPopover from './new_project_push_tip_popover.vue';
const BLANK_PANEL = 'blank_project';
const NEW_REPO_EXPERIMENT = 'new_repo';
const CI_CD_PANEL = 'cicd_for_external_repo';
const LAST_ACTIVE_TAB_KEY = 'new_project_last_active_tab';
const PANELS = [
{
key: 'blank',
name: BLANK_PANEL,
name: 'blank_project',
selector: '#blank-project-pane',
title: s__('ProjectsNew|Create blank project'),
description: s__(
......@@ -32,7 +28,7 @@ const PANELS = [
selector: '#create-from-template-pane',
title: s__('ProjectsNew|Create from template'),
description: s__(
'Create a project pre-populated with the necessary files to get you started quickly.',
'ProjectsNew|Create a project pre-populated with the necessary files to get you started quickly.',
),
illustration: createFromTemplateIllustration,
},
......@@ -42,7 +38,7 @@ const PANELS = [
selector: '#import-project-pane',
title: s__('ProjectsNew|Import project'),
description: s__(
'Migrate your data from an external source like GitHub, Bitbucket, or another instance of GitLab.',
'ProjectsNew|Migrate your data from an external source like GitHub, Bitbucket, or another instance of GitLab.',
),
illustration: importProjectIllustration,
},
......@@ -58,10 +54,8 @@ const PANELS = [
export default {
components: {
GlBreadcrumb,
GlIcon,
WelcomePage,
LegacyContainer,
NewNamespacePage,
NewProjectPushTipPopover,
},
directives: {
SafeHtml,
......@@ -84,12 +78,6 @@ export default {
},
},
data() {
return {
activeTab: null,
};
},
computed: {
decoratedPanels() {
const PANEL_TITLES = experiment(NEW_REPO_EXPERIMENT, {
......@@ -105,48 +93,15 @@ export default {
return PANELS.map(({ key, title, ...el }) => ({
...el,
title: PANEL_TITLES[key] !== undefined ? PANEL_TITLES[key] : title,
title: PANEL_TITLES[key] ?? title,
}));
},
availablePanels() {
if (this.isCiCdAvailable) {
return this.decoratedPanels;
}
return this.decoratedPanels.filter((p) => p.name !== CI_CD_PANEL);
return this.isCiCdAvailable
? this.decoratedPanels
: this.decoratedPanels.filter((p) => p.name !== CI_CD_PANEL);
},
activePanel() {
return this.decoratedPanels.find((p) => p.name === this.activeTab);
},
breadcrumbs() {
if (!this.activeTab || !this.activePanel) {
return null;
}
return [
{ text: __('New project'), href: '#' },
{ text: this.activePanel.title, href: `#${this.activeTab}` },
];
},
},
created() {
this.handleLocationHashChange();
if (this.hasErrors) {
this.activeTab = localStorage.getItem(LAST_ACTIVE_TAB_KEY) || BLANK_PANEL;
}
window.addEventListener('hashchange', () => {
this.handleLocationHashChange();
this.resetProjectErrors();
});
this.$root.$on('clicked::link', (e) => {
window.location = e.target.href;
});
},
methods: {
......@@ -156,46 +111,38 @@ export default {
errorsContainer.innerHTML = '';
}
},
handleLocationHashChange() {
this.activeTab = window.location.hash.substring(1) || null;
if (this.activeTab) {
localStorage.setItem(LAST_ACTIVE_TAB_KEY, this.activeTab);
}
},
},
PANELS,
EXPERIMENT: NEW_REPO_EXPERIMENT,
};
</script>
<template>
<welcome-page v-if="activeTab === null" :panels="availablePanels" />
<div v-else class="row">
<div class="col-lg-3">
<div class="gl-text-white" v-html="activePanel.illustration"></div>
<h4>{{ activePanel.title }}</h4>
<p>{{ activePanel.description }}</p>
<new-namespace-page
:initial-breadcrumb="s__('New project')"
:panels="availablePanels"
:jump-to-last-persisted-panel="hasErrors"
:title="s__('ProjectsNew|Create new project')"
:experiment="$options.EXPERIMENT"
persistence-key="new_project_last_active_tab"
@panel-change="resetProjectErrors"
>
<template #extra-description>
<div
v-if="newProjectGuidelines"
id="new-project-guideline"
v-safe-html="newProjectGuidelines"
></div>
</div>
<div class="col-lg-9">
<gl-breadcrumb v-if="breadcrumbs" :items="breadcrumbs">
<template #separator>
<gl-icon name="chevron-right" :size="8" />
</template>
</gl-breadcrumb>
<template v-for="panel in $options.PANELS">
<legacy-container
v-if="activeTab === panel.name"
:key="panel.name"
class="gl-mt-3"
:selector="panel.selector"
/>
</template>
</div>
</div>
</template>
<template #welcome-footer>
<div class="gl-pt-5 gl-text-center">
<p>
{{ __('You can also create a project from the command line.') }}
<a ref="clipTip" href="#" @click.prevent>
{{ __('Show command') }}
</a>
<new-project-push-tip-popover :target="() => $refs.clipTip" />
</p>
</div>
</template>
</new-namespace-page>
</template>
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { __ } from '~/locale';
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import initProjectVisibilitySelector from '../../../project_visibility';
import initProjectNew from '../../../projects/project_new';
import NewProjectCreationApp from './components/app.vue';
initProjectVisibilitySelector();
initProjectNew.bindEvents();
import(
/* webpackChunkName: 'experiment_new_project_creation' */ '../../../projects/experiment_new_project_creation'
)
.then((m) => {
const el = document.querySelector('.js-experiment-new-project-creation');
function initNewProjectCreation(el) {
const {
pushToCreateProjectCommand,
workingWithProjectsHelpPath,
newProjectGuidelines,
hasErrors,
isCiCdAvailable,
} = el.dataset;
if (!el) {
return;
}
const props = {
hasErrors: parseBoolean(hasErrors),
isCiCdAvailable: parseBoolean(isCiCdAvailable),
newProjectGuidelines,
};
const config = {
hasErrors: 'hasErrors' in el.dataset,
isCiCdAvailable: 'isCiCdAvailable' in el.dataset,
newProjectGuidelines: el.dataset.newProjectGuidelines,
};
m.default(el, config);
})
.catch(() => {
createFlash(__('An error occurred while loading project creation UI'));
const provide = {
workingWithProjectsHelpPath,
pushToCreateProjectCommand,
};
return new Vue({
el,
components: {
NewProjectCreationApp,
},
provide,
render(h) {
return h(NewProjectCreationApp, { props });
},
});
}
const el = document.querySelector('.js-new-project-creation');
initNewProjectCreation(el);
<script>
/* eslint-disable vue/no-v-html */
import Tracking from '~/tracking';
import { NEW_REPO_EXPERIMENT } from '../constants';
import NewProjectPushTipPopover from './new_project_push_tip_popover.vue';
const trackingMixin = Tracking.mixin({ ...gon.tracking_data, experiment: NEW_REPO_EXPERIMENT });
export default {
components: {
NewProjectPushTipPopover,
},
mixins: [trackingMixin],
props: {
panels: {
type: Array,
required: true,
},
},
};
</script>
<template>
<div class="container">
<div class="blank-state-welcome">
<h2 class="blank-state-welcome-title gl-mt-5! gl-mb-3!">
{{ s__('ProjectsNew|Create new project') }}
</h2>
<p div class="blank-state-text">&nbsp;</p>
</div>
<div class="row blank-state-row">
<a
v-for="panel in panels"
:key="panel.name"
:href="`#${panel.name}`"
:data-qa-selector="`${panel.name}_link`"
class="blank-state blank-state-link experiment-new-project-page-blank-state"
@click="track('click_tab', { label: panel.name })"
>
<div class="blank-state-icon gl-text-white" v-html="panel.illustration"></div>
<div class="blank-state-body gl-pl-4!">
<h3 class="blank-state-title experiment-new-project-page-blank-state-title">
{{ panel.title }}
</h3>
<p class="blank-state-text">
{{ panel.description }}
</p>
</div>
</a>
</div>
<div class="blank-state-welcome">
<p>
{{ __('You can also create a project from the command line.') }}
<a
ref="clipTip"
href="#"
click.prevent
class="push-new-project-tip"
rel="noopener noreferrer"
>
{{ __('Show command') }}
</a>
<new-project-push-tip-popover :target="() => $refs.clipTip" />
</p>
</div>
</div>
</template>
<svg width="82" height="80" viewBox="0 0 82 80" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M66.1912 8.19118H77.6176C78.2755 8.19118 78.8088 8.72448 78.8088 9.38235V69.6176C78.8088 70.2755 78.2755 70.8088 77.6176 70.8088H66.1912V8.19118Z" fill="#F0F0F0" stroke="#DBDBDB" stroke-width="2.38235"/>
<path d="M22.0517 19.2723L22.0094 10.1001C22.004 8.92546 22.8555 7.92221 24.0153 7.73664L63.3613 1.44139C64.8087 1.2098 66.12 2.32794 66.12 3.79382V75.8717C66.12 77.3323 64.8177 78.449 63.3742 78.2262L24.3037 72.1952C23.1461 72.0165 22.2902 71.023 22.2848 69.8517L22.2428 60.7554" stroke="#DBDBDB" stroke-width="2.38235"/>
<circle cx="23" cy="40" r="21" stroke="#6E49CB" stroke-width="2.38235"/>
<circle cx="23" cy="40" r="17" fill="#6E49CB"/>
<circle cx="23" cy="40" r="17" fill="white" fill-opacity="0.9"/>
<path d="M22.3125 48V33.3659" stroke="#6E49CB" stroke-width="2.38235" stroke-linecap="round"/>
<path d="M15 40.3049H30" stroke="#6E49CB" stroke-width="2.38235" stroke-linecap="round"/>
</svg>
<svg width="169" height="78" viewBox="0 0 169 78" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M115.571 41.5714L147.714 41.5714C158.365 41.5714 167 32.9369 167 22.2857C167 11.6345 158.365 3 147.714 3C137.063 3 128.429 11.6345 128.429 22.2857C128.429 27.3128 130.352 31.8907 133.503 35.3235" stroke="#DBDBDB" stroke-width="2.63203" stroke-linecap="round"/>
<path d="M115.107 41.5714H125.786C133.084 41.5714 139 47.4877 139 54.7857C139 62.0838 133.084 68 125.786 68C118.488 68 112.571 62.0838 112.571 54.7857C112.571 53.039 112.91 51.3715 113.526 49.8453" stroke="#DBDBDB" stroke-width="2.63203" stroke-linecap="round"/>
<path d="M87.5486 37H76.3943C75.6243 37 75 36.3746 75 35.6032C75 34.8318 75.6243 34.2064 76.3943 34.2064H87.5486C88.3187 34.2064 88.9429 34.8318 88.9429 35.6032C88.9429 36.3746 88.3187 37 87.5486 37Z" fill="#FC6D26"/>
<path d="M118.703 37H96.3943C95.6243 37 95 36.3746 95 35.6032C95 34.8318 95.6243 34.2064 96.3943 34.2064H118.703C119.473 34.2064 120.097 34.8318 120.097 35.6032C120.097 36.3746 119.473 37 118.703 37Z" fill="#FC6D26"/>
<path d="M118.703 37H96.3943C95.6243 37 95 36.3746 95 35.6032C95 34.8318 95.6243 34.2064 96.3943 34.2064H118.703C119.473 34.2064 120.097 34.8318 120.097 35.6032C120.097 36.3746 119.473 37 118.703 37Z" fill="white" fill-opacity="0.6"/>
<path d="M93.8573 32H71.3944C70.6243 32 70.0001 31.3746 70.0001 30.6032C70.0001 29.8318 70.6243 29.2064 71.3944 29.2064L93.8573 29.2064C94.6273 29.2064 95.2516 29.8318 95.2516 30.6032C95.2516 31.3746 94.6273 32 93.8573 32Z" fill="#6E49CB"/>
<path d="M93.8573 32H71.3944C70.6243 32 70.0001 31.3746 70.0001 30.6032C70.0001 29.8318 70.6243 29.2064 71.3944 29.2064L93.8573 29.2064C94.6273 29.2064 95.2516 29.8318 95.2516 30.6032C95.2516 31.3746 94.6273 32 93.8573 32Z" fill="white" fill-opacity="0.8"/>
<path d="M86.8573 49H71.3944C70.6243 49 70.0001 48.3746 70.0001 47.6032C70.0001 46.8317 70.6243 46.2064 71.3944 46.2064H86.8573C87.6273 46.2064 88.2516 46.8317 88.2516 47.6032C88.2516 48.3746 87.6273 49 86.8573 49Z" fill="#6E49CB"/>
<path d="M86.8573 49H71.3944C70.6243 49 70.0001 48.3746 70.0001 47.6032C70.0001 46.8317 70.6243 46.2064 71.3944 46.2064H86.8573C87.6273 46.2064 88.2516 46.8317 88.2516 47.6032C88.2516 48.3746 87.6273 49 86.8573 49Z" fill="white" fill-opacity="0.8"/>
<path d="M109.166 43L73.3944 43C72.6243 43 72.0001 42.3746 72.0001 41.6032C72.0001 40.8317 72.6243 40.2064 73.3944 40.2064L109.166 40.2064C109.936 40.2064 110.56 40.8317 110.56 41.6032C110.56 42.3746 109.936 43 109.166 43Z" fill="#6E49CB"/>
<path d="M109.166 43L73.3944 43C72.6243 43 72.0001 42.3746 72.0001 41.6032C72.0001 40.8317 72.6243 40.2064 73.3944 40.2064L109.166 40.2064C109.936 40.2064 110.56 40.8317 110.56 41.6032C110.56 42.3746 109.936 43 109.166 43Z" fill="white" fill-opacity="0.4"/>
<path d="M146.262 24.2349L143.048 21.0153C142.767 20.7338 142.282 20.7323 141.983 21.0313L140.394 22.6236C140.1 22.9181 140.088 23.4002 140.378 23.6903L145.344 28.6651C145.841 29.1637 146.666 29.1795 147.166 28.6793L147.866 27.9779L155.864 19.9653C156.171 19.658 156.167 19.1776 155.868 18.8786L154.279 17.2863C153.985 16.9918 153.495 16.9891 153.194 17.2903L146.262 24.2349Z" fill="#FC6D26"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M125.682 56.7113L123.087 59.3221C122.858 59.5529 122.547 59.6825 122.223 59.6824C121.898 59.6824 121.587 59.5526 121.358 59.3218C121.129 59.091 121 58.7779 121 58.4515C121 58.1251 121.129 57.8121 121.358 57.5813L123.087 55.8412L121.358 54.1011C121.129 53.8703 121 53.5573 121 53.2309C121 52.9045 121.129 52.5915 121.358 52.3606C121.587 52.1298 121.898 52.0001 122.223 52C122.547 51.9999 122.858 52.1296 123.087 52.3603L125.682 54.9711C125.911 55.2019 126.04 55.5149 126.04 55.8412C126.04 56.1675 125.911 56.4805 125.682 56.7113ZM131.796 56.7113L129.202 59.3221C129.088 59.4364 128.954 59.527 128.805 59.5888C128.657 59.6506 128.498 59.6824 128.337 59.6824C128.177 59.6824 128.018 59.6505 127.869 59.5886C127.721 59.5268 127.586 59.4361 127.472 59.3218C127.359 59.2075 127.269 59.0718 127.207 58.9225C127.146 58.7732 127.114 58.6131 127.114 58.4515C127.114 58.2899 127.146 58.1299 127.208 57.9806C127.269 57.8313 127.359 57.6956 127.473 57.5813L129.202 55.8412L127.473 54.1011C127.359 53.9868 127.269 53.8512 127.208 53.7018C127.146 53.5525 127.114 53.3925 127.114 53.2309C127.114 53.0693 127.146 52.9092 127.207 52.7599C127.269 52.6106 127.359 52.4749 127.472 52.3606C127.586 52.2463 127.721 52.1556 127.869 52.0938C128.018 52.0319 128.177 52 128.337 52C128.498 52 128.657 52.0318 128.805 52.0936C128.954 52.1554 129.088 52.246 129.202 52.3603L131.796 54.9711C132.026 55.2019 132.154 55.5149 132.154 55.8412C132.154 56.1675 132.026 56.4805 131.796 56.7113Z" fill="#6E49CB"/>
<path d="M2 26C2 28.415 14.4361 30.3727 29.7769 30.3727C33.7709 30.3727 37.568 30.24 41 30.0011" stroke="#DBDBDB" stroke-width="1.28173"/>
<path d="M2 50C2 52.415 14.4361 54.3727 29.7769 54.3727C35.6133 54.3727 41.0293 54.0893 45.5 53.6052" stroke="#DBDBDB" stroke-width="1.28173"/>
<path d="M57.5537 5V22M2 5V68.6673C2 73.1731 20.9696 75.5204 29.7769 75.5204C38.5842 75.5204 57.5537 73.1731 57.5537 68.6673V57" stroke="#DBDBDB" stroke-width="2.56346" stroke-linejoin="round"/>
<ellipse cx="29.7769" cy="5.64391" rx="27.7769" ry="3.64391" stroke="#DBDBDB" stroke-width="2.56346"/>
<ellipse cx="55.4286" cy="39.46" rx="17.4286" ry="17.46" stroke="#6E49CB" stroke-width="2.56346"/>
<ellipse cx="55.2458" cy="39.2696" rx="13.2458" ry="13.2696" fill="#6E49CB"/>
<ellipse cx="55.2458" cy="39.2696" rx="13.2458" ry="13.2696" fill="white" fill-opacity="0.9"/>
<path d="M61.763 38.5893C62.5797 39.0892 62.5797 40.2756 61.763 40.7756L52.951 46.1704C52.0969 46.6933 51 46.0787 51 45.0773L51 34.2875C51 33.2861 52.0969 32.6715 52.951 33.1944L61.763 38.5893Z" fill="#6E49CB"/>
</svg>
<svg width="82" height="80" viewBox="0 0 82 80" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M68.1765 8.17647H79.6471C80.2968 8.17647 80.8235 8.70319 80.8235 9.35294V69.6471C80.8235 70.2968 80.2968 70.8235 79.6471 70.8235H68.1765V8.17647Z" fill="#F0F0F0" stroke="#DBDBDB" stroke-width="2.35294"/>
<path d="M24.0504 19L24.0093 10.0746C24.0039 8.9145 24.8449 7.92363 25.9905 7.74035L65.393 1.43595C66.8226 1.20721 68.1176 2.31155 68.1176 3.75934V75.903C68.1176 77.3456 66.8314 78.4485 65.4057 78.2284L26.2788 72.1887C25.1356 72.0122 24.2902 71.0309 24.2849 69.8742L24.244 61" stroke="#DBDBDB" stroke-width="2.35294"/>
<path d="M60.0194 11.1796L30.0195 15.2198C29.4357 15.2984 29 15.7966 29 16.3857V19.1235C29 19.8153 29.594 20.3578 30.283 20.2951L60.283 17.5679C60.889 17.5128 61.3529 17.0047 61.3529 16.3962V12.3455C61.3529 11.6334 60.7252 11.0845 60.0194 11.1796Z" fill="#DBDBDB" stroke="#DBDBDB" stroke-width="0.588235" stroke-linecap="round" stroke-linejoin="bevel"/>
<path d="M51.1704 29.1021L41.8902 29.8481C41.0202 29.918 40.5266 30.8776 40.9756 31.626L42.6523 34.4205C42.8676 34.7793 43.2573 34.9968 43.6758 34.9916L51.2794 34.8968C51.9233 34.8888 52.4412 34.3645 52.4412 33.7205V30.2748C52.4412 29.5879 51.8551 29.0471 51.1704 29.1021Z" fill="#DBDBDB" stroke="#DBDBDB" stroke-width="0.588235" stroke-linecap="round" stroke-linejoin="bevel"/>
<path d="M61.2104 70.6341V40.1765C61.2104 39.5267 60.6837 39 60.0339 39H44.9909C44.4469 39 43.9738 39.373 43.8469 39.9019L41.118 51.2721C41.0819 51.4226 41.0148 51.5672 40.923 51.6918C37.1778 56.7763 34.7228 57.4741 29.7135 59.6826C29.2815 59.873 29.0064 60.3064 29.0162 60.7783L29.1309 66.295C29.1428 66.8693 29.5679 67.3511 30.1362 67.4345L59.8631 71.7981C60.5732 71.9024 61.2104 71.3519 61.2104 70.6341Z" fill="#DBDBDB" stroke="#DBDBDB" stroke-width="0.588235" stroke-linecap="round" stroke-linejoin="bevel"/>
<path d="M43.5694 24L36 24.5" stroke="#DBDBDB" stroke-width="1.17647" stroke-linecap="round"/>
<circle cx="23" cy="40" r="21" stroke="#6E49CB" stroke-width="2.35294"/>
<circle cx="23" cy="40" r="17" fill="#6E49CB"/>
<circle cx="23" cy="40" r="17" fill="white" fill-opacity="0.9"/>
<path d="M22.3125 48V33" stroke="#6E49CB" stroke-width="2.35294" stroke-linecap="round"/>
<path d="M15 41.3148H30" stroke="#6E49CB" stroke-width="2.35294" stroke-linecap="round"/>
</svg>
<svg width="169" height="84" viewBox="0 0 169 84" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path d="M153.5 74.5714H165.684C166.411 74.5714 167 73.9822 167 73.2554V8.74461C167 8.01779 166.411 7.42859 165.684 7.42859H153.5" stroke="#DBDBDB" stroke-width="2.63203"/>
<path d="M107.94 57L108.014 72.9062C108.017 73.5536 108.49 74.1026 109.13 74.2008L151.913 80.7674C152.71 80.8897 153.429 80.273 153.429 79.4666V2.54193C153.429 1.73264 152.705 1.11511 151.906 1.24226L108.829 8.09543C108.187 8.19744 107.716 8.7519 107.719 9.4012L107.771 20.5" stroke="#DBDBDB" stroke-width="2.63203"/>
<path d="M133.539 52.5313L122.91 51.9925M137.311 52.7225L148.969 53.3135" stroke="#DFDFDF" stroke-width="1.31602" stroke-linecap="round"/>
<path d="M132.224 43.9783L124 43.6955M135.998 44.1081L147.665 44.5092" stroke="#DFDFDF" stroke-width="1.31602" stroke-linecap="round"/>
<path d="M148.238 12.3644L131.189 14.604M117.282 16.4529L126.416 15.2311" stroke="#DFDFDF" stroke-width="1.31602" stroke-linecap="round"/>
<path d="M149.032 36.8519L131.839 37.0342M125 37.0852L127.024 37.0852" stroke="#DFDFDF" stroke-width="1.31602" stroke-linecap="round"/>
<path d="M112.038 66.3444L120.582 67.4102M148.266 70.8634L134.595 69.1581M125.025 67.9644L129.468 68.5186" stroke="#DFDFDF" stroke-width="1.31602" stroke-linecap="round"/>
<path d="M114.352 23.3947L116.215 23.2387M129.258 22.147L119.433 22.9693M137.388 21.4665L145.18 20.8143" stroke="#DFDFDF" stroke-width="1.31602" stroke-linecap="round"/>
<path d="M135.832 29.2067L125.981 29.5888M138.724 28.9864L146.537 28.6833" stroke="#DFDFDF" stroke-width="1.31602" stroke-linecap="round"/>
<path d="M115.114 59.5557L128.942 60.8796M133.782 61.3429L145.19 62.4351" stroke="#DFDFDF" stroke-width="1.31602" stroke-linecap="round"/>
<path d="M53.4286 42.4286H21.2857C10.6345 42.4286 2.00002 33.7941 2.00002 23.1429C2.00002 12.4917 10.6345 3.85718 21.2857 3.85718C31.9369 3.85718 40.5714 12.4917 40.5714 23.1429C40.5714 28.17 38.648 32.7479 35.4969 36.1807" stroke="#DBDBDB" stroke-width="2.63203" stroke-linecap="round"/>
<path d="M53.0361 42.4286H42.3571C35.0591 42.4286 29.1428 48.3448 29.1428 55.6429C29.1428 62.9409 35.0591 68.8572 42.3571 68.8572C49.6552 68.8572 55.5714 62.9409 55.5714 55.6429C55.5714 53.8962 55.2325 52.2287 54.6169 50.7025" stroke="#DBDBDB" stroke-width="2.63203" stroke-linecap="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M38.4286 51.7144C38.4286 50.9254 39.0682 50.2858 39.8572 50.2858H44.1429C44.829 50.2858 45.4022 50.7695 45.5399 51.4146L47.7105 52.6677C48.3938 53.0622 48.6279 53.9359 48.2334 54.6192C47.3183 56.2042 45.5714 59.2248 45.4609 59.4191C45.1836 59.9063 44.7237 60.2858 44.1429 60.2858H39.8572C39.0682 60.2858 38.4286 59.6462 38.4286 58.8572V51.7144ZM39.8572 51.7144H44.1429V58.8572H39.8572L39.8572 51.7144ZM45.5714 56.3727L46.9962 53.9049L45.5714 53.0823V56.3727Z" fill="#FC6D26"/>
<path d="M25.5984 15.2331C25.8026 14.471 25.3503 13.6877 24.5882 13.4835C23.8261 13.2793 23.0428 13.7315 22.8386 14.4936L18.4017 31.0524C18.1975 31.8145 18.6497 32.5978 19.4118 32.802C20.1739 33.0062 20.9573 32.5539 21.1615 31.7918L25.5984 15.2331Z" fill="#6E49CB"/>
<path d="M17.2958 17.8469C17.8537 18.4048 17.8537 19.3093 17.2958 19.8672L14.0203 23.1428L17.2958 26.4183C17.8537 26.9762 17.8537 27.8807 17.2958 28.4386C16.738 28.9965 15.8334 28.9965 15.2755 28.4386L10.9898 24.1529C10.4319 23.595 10.4319 22.6905 10.9898 22.1326L15.2755 17.8469C15.8334 17.289 16.738 17.289 17.2958 17.8469Z" fill="#6E49CB"/>
<path d="M26.7041 17.8469C26.1462 18.4048 26.1462 19.3093 26.7041 19.8672L29.9797 23.1428L26.7041 26.4183C26.1462 26.9762 26.1462 27.8807 26.7041 28.4386C27.262 28.9965 28.1665 28.9965 28.7244 28.4386L33.0101 24.1529C33.568 23.595 33.568 22.6905 33.0101 22.1326L28.7244 17.8469C28.1665 17.289 27.262 17.289 26.7041 17.8469Z" fill="#6E49CB"/>
<path d="M50.5714 35.2857L62 35.2857C62.7889 35.2857 63.4285 35.9253 63.4285 36.7143C63.4285 37.5032 62.7889 38.1428 62 38.1428L50.5714 38.1428C49.7824 38.1428 49.1428 37.5032 49.1428 36.7143C49.1428 35.9253 49.7824 35.2857 50.5714 35.2857Z" fill="#FC6D26"/>
<path d="M50.5714 35.2857L62 35.2857C62.7889 35.2857 63.4285 35.9253 63.4285 36.7143C63.4285 37.5032 62.7889 38.1428 62 38.1428L50.5714 38.1428C49.7824 38.1428 49.1428 37.5032 49.1428 36.7143C49.1428 35.9253 49.7824 35.2857 50.5714 35.2857Z" fill="white" fill-opacity="0.6"/>
<path d="M70.5713 35.2857L83.4285 35.2857C84.2175 35.2857 84.8571 35.9253 84.8571 36.7143C84.8571 37.5032 84.2175 38.1428 83.4285 38.1428L70.5713 38.1428C69.7824 38.1428 69.1428 37.5032 69.1428 36.7143C69.1428 35.9253 69.7824 35.2857 70.5713 35.2857Z" fill="#FC6D26"/>
<path d="M76.2856 46.7144L92.1428 46.7144C92.9318 46.7144 93.5714 47.3539 93.5714 48.1429C93.5714 48.9319 92.9318 49.5715 92.1428 49.5715L76.2856 49.5715C75.4967 49.5715 74.8571 48.9319 74.8571 48.1429C74.8571 47.354 75.4967 46.7144 76.2856 46.7144Z" fill="#6E49CB"/>
<path d="M76.2856 46.7144L92.1428 46.7144C92.9318 46.7144 93.5714 47.3539 93.5714 48.1429C93.5714 48.9319 92.9318 49.5715 92.1428 49.5715L76.2856 49.5715C75.4967 49.5715 74.8571 48.9319 74.8571 48.1429C74.8571 47.354 75.4967 46.7144 76.2856 46.7144Z" fill="white" fill-opacity="0.8"/>
<path d="M62.7142 40.9999L90 40.9999C90.7889 40.9999 91.4285 41.6395 91.4285 42.4285C91.4285 43.2175 90.7889 43.8571 90 43.8571L62.7142 43.8571C61.9253 43.8571 61.2857 43.2175 61.2857 42.4285C61.2857 41.6395 61.9253 40.9999 62.7142 40.9999Z" fill="#6E49CB"/>
<path d="M62.7142 40.9999L90 40.9999C90.7889 40.9999 91.4285 41.6395 91.4285 42.4285C91.4285 43.2175 90.7889 43.8571 90 43.8571L62.7142 43.8571C61.9253 43.8571 61.2857 43.2175 61.2857 42.4285C61.2857 41.6395 61.9253 40.9999 62.7142 40.9999Z" fill="white" fill-opacity="0.6"/>
<path d="M69.8571 29.5714L91.5714 29.5714C92.3603 29.5714 92.9999 30.211 92.9999 31C92.9999 31.789 92.3603 32.4286 91.5714 32.4286L69.8571 32.4286C69.0681 32.4286 68.4285 31.789 68.4285 31C68.4285 30.211 69.0681 29.5714 69.8571 29.5714Z" fill="#6E49CB"/>
<path d="M69.8571 29.5714L91.5714 29.5714C92.3603 29.5714 92.9999 30.211 92.9999 31C92.9999 31.789 92.3603 32.4286 91.5714 32.4286L69.8571 32.4286C69.0681 32.4286 68.4285 31.789 68.4285 31C68.4285 30.211 69.0681 29.5714 69.8571 29.5714Z" fill="white" fill-opacity="0.8"/>
<circle cx="107.714" cy="38.8571" r="17.8571" stroke="#6E49CB" stroke-width="2.63203"/>
<circle cx="107.714" cy="38.8573" r="13.5714" fill="#6E49CB"/>
<circle cx="107.714" cy="38.8573" r="13.5714" fill="white" fill-opacity="0.9"/>
<path d="M111.431 35.0867L115.367 39.0232L111.431 42.9597C111.016 43.3744 110.344 43.3744 109.929 42.9597C109.515 42.545 109.515 41.8727 109.929 41.458L111.302 40.0851H101.123C100.537 40.0851 100.061 39.6097 100.061 39.0232C100.061 38.4367 100.537 37.9613 101.123 37.9613H111.302L109.929 36.5884C109.515 36.1737 109.515 35.5014 109.929 35.0867C110.344 34.672 111.016 34.672 111.431 35.0867Z" fill="#6E49CB"/>
</g>
<defs>
<clipPath id="clip0">
<rect width="169" height="84" fill="white"/>
</clipPath>
</defs>
</svg>
import Vue from 'vue';
import NewProjectCreationApp from './components/app.vue';
export default function initNewProjectCreation(el, props) {
const { pushToCreateProjectCommand, workingWithProjectsHelpPath } = el.dataset;
return new Vue({
el,
components: {
NewProjectCreationApp,
},
provide: {
workingWithProjectsHelpPath,
pushToCreateProjectCommand,
},
render(h) {
return h(NewProjectCreationApp, { props });
},
});
}
<script>
import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import Vue from 'vue';
import Tracking from '~/tracking';
export default {
directives: {
SafeHtml,
},
props: {
title: {
type: String,
required: true,
},
panels: {
type: Array,
required: true,
},
experiment: {
type: String,
required: false,
default: null,
},
},
created() {
const trackingMixin = Tracking.mixin({ ...gon.tracking_data, experiment: this.experiment });
const trackingInstance = new Vue({
...trackingMixin,
render() {
return null;
},
});
this.track = trackingInstance.track;
},
};
</script>
<template>
<div class="container">
<h2 class="gl-my-7 gl-font-size-h1 gl-text-center">
{{ title }}
</h2>
<div>
<div
v-for="panel in panels"
:key="panel.name"
class="new-namespace-panel-wrapper gl-display-inline-block gl-px-3 gl-mb-5"
>
<a
:href="`#${panel.name}`"
:data-qa-selector="`${panel.name}_link`"
class="new-namespace-panel gl-display-flex gl-flex-shrink-0 gl-flex-direction-column gl-lg-flex-direction-row gl-align-items-center gl-rounded-base gl-border-gray-100 gl-border-solid gl-border-1 gl-w-full gl-py-6 gl-px-8 gl-hover-text-decoration-none!"
@click="track('click_tab', { label: panel.name })"
>
<div
v-safe-html="panel.illustration"
class="new-namespace-panel-illustration gl-text-white gl-display-flex gl-flex-shrink-0 gl-justify-content-center"
></div>
<div class="gl-pl-4">
<h3 class="gl-font-size-h2 gl-reset-color">
{{ panel.title }}
</h3>
<p class="gl-text-gray-900">
{{ panel.description }}
</p>
</div>
</a>
</div>
</div>
<slot name="footer"></slot>
</div>
</template>
<script>
import { GlBreadcrumb, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import LegacyContainer from './components/legacy_container.vue';
import WelcomePage from './components/welcome.vue';
export default {
components: {
GlBreadcrumb,
GlIcon,
WelcomePage,
LegacyContainer,
},
directives: {
SafeHtml,
},
props: {
title: {
type: String,
required: true,
},
initialBreadcrumb: {
type: String,
required: true,
},
panels: {
type: Array,
required: true,
},
jumpToLastPersistedPanel: {
type: Boolean,
required: false,
default: false,
},
persistenceKey: {
type: String,
required: true,
},
experiment: {
type: String,
required: false,
default: null,
},
},
data() {
return {
activePanelName: null,
};
},
computed: {
activePanel() {
return this.panels.find((p) => p.name === this.activePanelName);
},
details() {
return this.activePanel.details || this.activePanel.description;
},
hasTextDetails() {
return typeof this.details === 'string';
},
breadcrumbs() {
if (!this.activePanel) {
return null;
}
return [
{ text: this.initialBreadcrumb, href: '#' },
{ text: this.activePanel.title, href: `#${this.activePanel.name}` },
];
},
},
created() {
this.handleLocationHashChange();
if (this.jumpToLastPersistedPanel) {
this.activePanelName = localStorage.getItem(this.persistenceKey) || this.panels[0].name;
}
window.addEventListener('hashchange', () => {
this.handleLocationHashChange();
this.$emit('panel-change');
});
this.$root.$on('clicked::link', (e) => {
window.location = e.target.href;
});
},
methods: {
handleLocationHashChange() {
this.activePanelName = window.location.hash.substring(1) || null;
if (this.activePanelName) {
localStorage.setItem(this.persistenceKey, this.activePanelName);
}
},
},
};
</script>
<template>
<welcome-page
v-if="activePanelName === null"
:panels="panels"
:title="title"
:experiment="experiment"
>
<template #footer>
<slot name="welcome-footer"> </slot>
</template>
</welcome-page>
<div v-else class="row">
<div class="col-lg-3">
<div v-safe-html="activePanel.illustration" class="gl-text-white"></div>
<h4>{{ activePanel.title }}</h4>
<p v-if="hasTextDetails">{{ details }}</p>
<component :is="details" v-else />
<slot name="extra-description"></slot>
</div>
<div class="col-lg-9">
<gl-breadcrumb v-if="breadcrumbs" :items="breadcrumbs">
<template #separator>
<gl-icon name="chevron-right" :size="8" />
</template>
</gl-breadcrumb>
<legacy-container :key="activePanel.name" class="gl-mt-3" :selector="activePanel.selector" />
</div>
</div>
</template>
......@@ -45,7 +45,6 @@
@import 'framework/toggle';
@import 'framework/typography';
@import 'framework/zen';
@import 'framework/blank';
@import 'framework/wells';
@import 'framework/page_header';
@import 'framework/page_title';
......
.blank-state-parent-container {
.section-container {
padding: 10px;
}
.section-body {
width: 100%;
height: 100%;
padding-bottom: 25px;
border-radius: $border-radius-default;
}
}
.blank-state-row {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.blank-state-welcome {
text-align: center;
padding: $gl-padding 0 ($gl-padding * 2);
.blank-state-welcome-title {
font-size: 24px;
}
.blank-state-text {
margin-bottom: 0;
}
}
.blank-state-link {
color: $gl-text-color;
margin-bottom: 15px;
&:hover {
background-color: $gray-light;
text-decoration: none;
color: $gl-text-color;
}
}
.blank-state-center {
padding-top: 20px;
padding-bottom: 20px;
text-align: center;
}
.blank-state {
display: flex;
align-items: center;
padding: 20px 50px;
border: 1px solid $border-color;
border-radius: $border-radius-default;
min-height: 240px;
margin-bottom: $gl-padding;
width: calc(50% - #{$gl-padding-8});
@include media-breakpoint-down(sm) {
width: 100%;
flex-direction: column;
justify-content: center;
padding: 50px 20px;
.column-small & {
width: 100%;
}
}
}
.blank-state,
.blank-state-center {
.blank-state-icon {
svg {
display: block;
margin: auto;
}
}
.blank-state-title {
margin-top: 0;
font-size: 18px;
}
.blank-state-body {
@include media-breakpoint-down(sm) {
text-align: center;
margin-top: 20px;
}
@include media-breakpoint-up(sm) {
padding-left: 20px;
}
}
}
@include media-breakpoint-up(lg) {
.column-large {
flex: 2;
}
.column-small {
flex: 1;
margin-bottom: 15px;
.blank-state {
max-width: 400px;
flex-wrap: wrap;
margin-left: 15px;
}
.blank-state-icon {
margin-bottom: 30px;
}
}
}
.experiment-new-project-page-blank-state {
@include media-breakpoint-down(md) {
flex-direction: column;
justify-content: center;
text-align: center;
}
.blank-state-icon {
min-width: 215px;
}
}
$experiment-new-project-indigo-700: #41419f;
.experiment-new-project-page-blank-state-title {
color: $experiment-new-project-indigo-700;
}
@import 'mixins_and_variables_and_functions';
$new-namespace-panel-illustration-width: 215px;
$new-namespace-panel-height: 240px;
.new-namespace-panel-illustration {
width: $new-namespace-panel-illustration-width;
}
.new-namespace-panel-wrapper {
@include media-breakpoint-down(md) {
width: 100%;
}
width: 50%;
}
.new-namespace-panel {
&:hover {
background-color: $gray-10;
}
color: $purple-700;
min-height: $new-namespace-panel-height;
text-align: center;
@include media-breakpoint-up(lg) {
text-align: left;
}
}
......@@ -85,7 +85,7 @@ class ProjectsController < Projects::ApplicationController
notice: _("Project '%{project_name}' was successfully created.") % { project_name: @project.name }
)
else
render 'new', locals: { active_tab: active_new_project_tab }
render 'new'
end
end
......
......@@ -2,80 +2,38 @@
- @hide_top_links = true
- page_title _('New Project')
- header_title _("Projects"), dashboard_projects_path
- active_tab = local_assigns.fetch(:active_tab, 'blank')
- add_page_specific_style 'page_bundles/new_namespace'
.project-edit-container.gl-mt-5
.project-edit-errors
= render 'projects/errors'
.js-experiment-new-project-creation{ data: { is_ci_cd_available: (ci_cd_projects_available? if Gitlab.ee?), has_errors: @project.errors.any?, new_project_guidelines: brand_new_project_guidelines, push_to_create_project_command: push_to_create_project_command, working_with_projects_help_path: help_page_path("user/project/working_with_projects") } }
.js-new-project-creation{ data: { is_ci_cd_available: (ci_cd_projects_available? if Gitlab.ee?).to_s, has_errors: @project.errors.any?.to_s, new_project_guidelines: brand_new_project_guidelines, push_to_create_project_command: push_to_create_project_command, working_with_projects_help_path: help_page_path("user/project/working_with_projects") } }
.row{ 'v-cloak': true }
.col-lg-3.profile-settings-sidebar
%h4.gl-mt-0
= _('New project')
%p
- among_other_things_link = link_to _('among other things'), help_page_path("user/project/index.md", anchor: "project-features"), target: '_blank'
= _('A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}.').html_safe % { among_other_things_link: among_other_things_link }
%p
= _('All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings.')
= render_if_exists 'projects/new_ci_cd_banner_external_repo'
%p
- pages_getting_started_guide = link_to _('Pages getting started guide'), help_page_path("user/project/pages/index", anchor: "getting-started"), target: '_blank'
= _('Information about additional Pages templates and how to install them can be found in our %{pages_getting_started_guide}.').html_safe % { pages_getting_started_guide: pages_getting_started_guide }
.md
= brand_new_project_guidelines
%p
%strong= _("Tip:")
= _("You can also create a project from the command line.")
.col-lg-9.js-toggle-container
%ul.nav.nav-tabs.nav-links.gitlab-tabs{ role: 'tablist' }
%li.nav-item{ role: 'presentation' }
%a.nav-link.active{ href: '#blank-project-pane', id: 'blank-project-tab', data: { toggle: 'tab', experiment_track_label: 'blank_project' }, role: 'tab' }
%span.d-none.d-sm-block= s_('ProjectsNew|Blank project')
%span.d-block.d-sm-none= s_('ProjectsNew|Blank')
%li.nav-item{ role: 'presentation' }
%a.nav-link{ href: '#create-from-template-pane', id: 'create-from-template-tab', data: { toggle: 'tab', experiment_track_label: 'create_from_template' }, role: 'tab' }
%span.d-none.d-sm-block.qa-project-create-from-template-tab= s_('ProjectsNew|Create from template')
%span.d-block.d-sm-none= s_('ProjectsNew|Template')
%li.nav-item{ role: 'presentation' }
%a.nav-link{ href: '#import-project-pane', id: 'import-project-tab', data: { toggle: 'tab', experiment_track_label: 'import_project' }, role: 'tab' }
%span.d-none.d-sm-block= s_('ProjectsNew|Import project')
%span.d-block.d-sm-none= s_('ProjectsNew|Import')
= render_if_exists 'projects/new_ci_cd_only_project_tab', active_tab: active_tab
.tab-content.gitlab-tab-content
.tab-pane.js-toggle-container{ id: 'blank-project-pane', class: active_when(active_tab == 'blank'), role: 'tabpanel' }
= form_for @project, html: { class: 'new_project' } do |f|
= render 'new_project_fields', f: f, project_name_id: "blank-project-name"
#create-from-template-pane.tab-pane.js-toggle-container.px-0.pb-0{ class: active_when(active_tab == 'template'), role: 'tabpanel' }
.card.card-slim.m-4.p-4
#blank-project-pane.tab-pane.active
= form_for @project, html: { class: 'new_project' } do |f|
= render 'new_project_fields', f: f, project_name_id: "blank-project-name"
#create-from-template-pane.tab-pane
.gl-card.gl-my-5
.gl-card-body
%div
- contributing_templates_url = 'https://gitlab.com/gitlab-org/project-templates/contributing'
- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: contributing_templates_url }
= _('Learn how to %{link_start}contribute to the built-in templates%{link_end}').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
= form_for @project, html: { class: 'new_project' } do |f|
.project-template
.form-group
%div
- contributing_templates_url = 'https://gitlab.com/gitlab-org/project-templates/contributing'
- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: contributing_templates_url }
= _('Learn how to %{link_start}contribute to the built-in templates%{link_end}').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
= form_for @project, html: { class: 'new_project' } do |f|
.project-template
.form-group
%div
= render 'project_templates', f: f, project: @project
.tab-pane.import-project-pane.js-toggle-container{ id: 'import-project-pane', class: active_when(active_tab == 'import'), role: 'tabpanel' }
- if import_sources_enabled?
= render 'import_project_pane', active_tab: active_tab
- else
.nothing-here-block
%h4= s_('ProjectsNew|No import options available')
%p= s_('ProjectsNew|Contact an administrator to enable options for importing your project.')
= render 'project_templates', f: f, project: @project
= render_if_exists 'projects/new_ci_cd_only_project_pane', active_tab: active_tab
#import-project-pane.tab-pane.js-toggle-container
- if import_sources_enabled?
= render 'import_project_pane'
- else
.nothing-here-block
%h4= s_('ProjectsNew|No import options available')
%p= s_('ProjectsNew|Contact an administrator to enable options for importing your project.')
.save-project-loader.d-none
.center
%h2
.gl-spinner.gl-spinner-md.align-text-bottom
= s_('ProjectsNew|Creating project & repository.')
%p
= s_('ProjectsNew|Please wait a moment, this page will automatically refresh when ready.')
= render_if_exists 'projects/new_ci_cd_only_project_pane'
---
title: Make new project ui the only option
merge_request: 59452
author:
type: changed
......@@ -207,6 +207,7 @@ module Gitlab
config.assets.precompile << "page_bundles/merge_conflicts.css"
config.assets.precompile << "page_bundles/merge_requests.css"
config.assets.precompile << "page_bundles/milestone.css"
config.assets.precompile << "page_bundles/new_namespace.css"
config.assets.precompile << "page_bundles/oncall_schedules.css"
config.assets.precompile << "page_bundles/pipeline.css"
config.assets.precompile << "page_bundles/pipeline_schedules.css"
......
......@@ -41,7 +41,8 @@ For a list of words that can't be used as project names see
To create a new blank project on the **New project** page:
1. On the **Blank project** tab, provide the following information:
1. Click **Create blank project**
1. Provide the following information:
- The name of your project in the **Project name** field. You can't use
special characters, but you can use spaces, hyphens, underscores, or even
emoji. When adding the name, the **Project slug** auto populates.
......@@ -86,7 +87,8 @@ Built-in templates are project templates that are:
To use a built-in template on the **New project** page:
1. On the **Create from template** tab, select the **Built-in** tab.
1. Click **Create from template**
1. Select the **Built-in** tab.
1. From the list of available built-in templates, click the:
- **Preview** button to look at the template source itself.
- **Use template** button to start creating the project.
......@@ -99,7 +101,8 @@ GitLab is developing Enterprise templates to help you streamline audit managemen
To create a new project with an Enterprise template, on the **New project** page:
1. On the **Create from template** tab, select the **Built-in** tab.
1. Click **Create from template**
1. Select the **Built-in** tab.
1. From the list of available built-in Enterprise templates, click the:
- **Preview** button to look at the template source itself.
- **Use template** button to start creating the project.
......@@ -123,11 +126,12 @@ quickly starting projects.
Custom projects are available at the [instance-level](../../user/admin_area/custom_project_templates.md)
from the **Instance** tab, or at the [group-level](../../user/group/custom_project_templates.md)
from the **Group** tab, under the **Create from template** tab.
from the **Group** tab, on the **Create from template** page.
To use a custom project template on the **New project** page:
1. On the **Create from template** tab, select the **Instance** tab or the **Group** tab.
1. Click **Create from template**
1. Select the **Instance** tab or the **Group** tab.
1. From the list of available custom templates, click the:
- **Preview** button to look at the template source itself.
- **Use template** button to start creating the project.
......
- return unless ci_cd_projects_available?
- track_label = local_assigns.fetch(:track_label, 'cicd_for_external_repo')
.tab-pane.js-toggle-container{ id: 'ci-cd-project-pane', class: active_when(active_tab == 'ci_cd_only'), role: 'tabpanel' }
#ci-cd-project-pane.tab-pane
= form_for @project, html: { class: 'new_project' } do |f|
.project-import.row
.col-lg-12
......@@ -28,7 +28,7 @@
= sprite_icon('link', css_class: 'gl-button-icon')
= _('Repo by URL')
.col-lg-12
.js-toggle-content.toggle-import-form{ class: ('hide' if active_tab != 'ci_cd_only') }
.js-toggle-content.toggle-import-form
%hr
= render "shared/import_form", f: f, ci_cd_only: true
= render 'new_project_fields', f: f, project_name_id: "import-url-name", ci_cd_only: true, hide_init_with_readme: true, track_label: track_label
- return unless ci_cd_projects_available?
%li.nav-item{ class: active_when(active_tab == 'ci_cd_only'), role: 'presentation' }
%a.nav-link{ href: '#ci-cd-project-pane', id: 'ci-cd-project-tab', data: { qa_selector: 'ci_cd_project_tab', toggle: 'tab', experiment_track_label: 'cicd_for_external_repo' }, role: 'tab' }
%span.d-none.d-sm-block
= _('CI/CD for external repo')
%span.d-block.d-sm-none
= _('CI/CD')
......@@ -1463,9 +1463,6 @@ msgstr ""
msgid "A project containing issues for each audit inquiry in the HIPAA Audit Protocol published by the U.S. Department of Health & Human Services"
msgstr ""
msgid "A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}."
msgstr ""
msgid "A project’s repository name defines its URL (the one you use to access the project via a browser) and its place on the file disk where GitLab is installed. %{link_start}Learn more.%{link_end}"
msgstr ""
......@@ -3184,9 +3181,6 @@ msgstr ""
msgid "All epics"
msgstr ""
msgid "All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings."
msgstr ""
msgid "All groups and projects"
msgstr ""
......@@ -3595,9 +3589,6 @@ msgstr ""
msgid "An error occurred while loading merge requests."
msgstr ""
msgid "An error occurred while loading project creation UI"
msgstr ""
msgid "An error occurred while loading the access tokens form, please try again."
msgstr ""
......@@ -5592,9 +5583,6 @@ msgstr ""
msgid "CI/CD configuration file"
msgstr ""
msgid "CI/CD for external repo"
msgstr ""
msgid "CICDAnalytics|%{percent}%{percentSymbol}"
msgstr ""
......@@ -9250,9 +9238,6 @@ msgstr ""
msgid "Create a personal access token on your account to pull or push via %{protocol}."
msgstr ""
msgid "Create a project pre-populated with the necessary files to get you started quickly."
msgstr ""
msgid "Create an account using:"
msgstr ""
......@@ -17356,9 +17341,6 @@ msgstr ""
msgid "Inform users without uploaded SSH keys that they can't push over SSH until one is added"
msgstr ""
msgid "Information about additional Pages templates and how to install them can be found in our %{pages_getting_started_guide}."
msgstr ""
msgid "Infrastructure"
msgstr ""
......@@ -20925,9 +20907,6 @@ msgstr ""
msgid "Middleman project with Static Site Editor support"
msgstr ""
msgid "Migrate your data from an external source like GitHub, Bitbucket, or another instance of GitLab."
msgstr ""
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
......@@ -23404,9 +23383,6 @@ msgstr ""
msgid "Pages Domain"
msgstr ""
msgid "Pages getting started guide"
msgstr ""
msgid "Pagination|Go to first page"
msgstr ""
......@@ -25828,12 +25804,6 @@ msgstr ""
msgid "ProjectsNew|Allows you to immediately clone this project’s repository. Skip this if you plan to push up an existing repository."
msgstr ""
msgid "ProjectsNew|Blank"
msgstr ""
msgid "ProjectsNew|Blank project"
msgstr ""
msgid "ProjectsNew|Connect your external repository to GitLab CI/CD."
msgstr ""
......@@ -25846,6 +25816,9 @@ msgstr ""
msgid "ProjectsNew|Create a blank project to house your files, plan your work, and collaborate on code, among other things."
msgstr ""
msgid "ProjectsNew|Create a project pre-populated with the necessary files to get you started quickly."
msgstr ""
msgid "ProjectsNew|Create blank project"
msgstr ""
......@@ -25858,9 +25831,6 @@ msgstr ""
msgid "ProjectsNew|Create new project"
msgstr ""
msgid "ProjectsNew|Creating project & repository."
msgstr ""
msgid "ProjectsNew|Description format"
msgstr ""
......@@ -25876,10 +25846,10 @@ msgstr ""
msgid "ProjectsNew|Initialize repository with a README"
msgstr ""
msgid "ProjectsNew|No import options available"
msgid "ProjectsNew|Migrate your data from an external source like GitHub, Bitbucket, or another instance of GitLab."
msgstr ""
msgid "ProjectsNew|Please wait a moment, this page will automatically refresh when ready."
msgid "ProjectsNew|No import options available"
msgstr ""
msgid "ProjectsNew|Project description %{tag_start}(optional)%{tag_end}"
......@@ -25888,9 +25858,6 @@ msgstr ""
msgid "ProjectsNew|Run CI/CD for external repository"
msgstr ""
msgid "ProjectsNew|Template"
msgstr ""
msgid "ProjectsNew|Visibility Level"
msgstr ""
......@@ -33489,9 +33456,6 @@ msgstr[1] ""
msgid "Time|s"
msgstr ""
msgid "Tip:"
msgstr ""
msgid "Tip: Hover over a job to see the jobs it depends on to run."
msgstr ""
......@@ -37447,9 +37411,6 @@ msgstr ""
msgid "already shared with this group"
msgstr ""
msgid "among other things"
msgstr ""
msgid "and"
msgstr ""
......
......@@ -260,7 +260,6 @@ module QA
module Project
autoload :New, 'qa/page/project/new'
autoload :NewExperiment, 'qa/page/project/new_experiment'
autoload :Show, 'qa/page/project/show'
autoload :Activity, 'qa/page/project/activity'
autoload :Menu, 'qa/page/project/menu'
......
......@@ -28,7 +28,7 @@ module QA
element :template_option_row
end
view 'app/assets/javascripts/projects/experiment_new_project_creation/components/welcome.vue' do
view 'app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue' do
element :cicd_for_external_repo_link, ':data-qa-selector="`${panel.name}_link`"' # rubocop:disable QA/ElementWithPattern
end
end
......
......@@ -6,11 +6,7 @@ module QA
module_function
def go_to_create_project_from_template
if Page::Project::NewExperiment.perform(&:shown?)
Page::Project::NewExperiment.perform(&:click_create_from_template_link)
else
Page::Project::New.perform(&:click_create_from_template_tab)
end
Page::Project::New.perform(&:click_create_from_template_link)
end
end
end
......
......@@ -8,11 +8,6 @@ module QA
include Page::Component::Select2
include Page::Component::VisibilitySetting
view 'app/views/projects/new.html.haml' do
element :project_create_from_template_tab
element :import_project_tab, "Import project" # rubocop:disable QA/ElementWithPattern
end
view 'app/views/projects/_new_project_fields.html.haml' do
element :initialize_with_readme_checkbox
element :project_namespace_select
......@@ -29,6 +24,19 @@ module QA
element :template_option_row
end
view 'app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue' do
element :blank_project_link, ':data-qa-selector="`${panel.name}_link`"' # rubocop:disable QA/ElementWithPattern
element :create_from_template_link, ':data-qa-selector="`${panel.name}_link`"' # rubocop:disable QA/ElementWithPattern
end
def click_blank_project_link
click_element :blank_project_link
end
def click_create_from_template_link
click_element :create_from_template_link
end
def choose_test_namespace
choose_namespace(Runtime::Namespace.path)
end
......@@ -60,6 +68,10 @@ module QA
click_element(:project_create_from_template_tab)
end
def set_visibility(visibility)
choose visibility.capitalize
end
def click_github_link
click_link 'GitHub'
end
......
# frozen_string_literal: true
module QA
module Page
module Project
class NewExperiment < Page::Base
view 'app/assets/javascripts/projects/experiment_new_project_creation/components/welcome.vue' do
element :blank_project_link, ':data-qa-selector="`${panel.name}_link`"' # rubocop:disable QA/ElementWithPattern
element :create_from_template_link, ':data-qa-selector="`${panel.name}_link`"' # rubocop:disable QA/ElementWithPattern
end
def shown?
has_element? :blank_project_link
end
def click_blank_project_link
click_element :blank_project_link
end
def click_create_from_template_link
click_element :create_from_template_link
end
end
end
end
end
......@@ -83,7 +83,7 @@ module QA
end
end
Page::Project::NewExperiment.perform(&:click_blank_project_link) if Page::Project::NewExperiment.perform(&:shown?)
Page::Project::New.perform(&:click_blank_project_link)
Page::Project::New.perform do |new_page|
new_page.choose_test_namespace
......
......@@ -37,7 +37,7 @@ RSpec.describe 'Admin Appearance' do
expect_custom_sign_in_appearance(appearance)
end
it 'preview new project page appearance' do
it 'preview new project page appearance', :js do
sign_in(admin)
gitlab_enable_admin_mode_sign_in(admin)
......@@ -86,10 +86,11 @@ RSpec.describe 'Admin Appearance' do
expect_custom_sign_in_appearance(appearance)
end
it 'custom new project page' do
it 'custom new project page', :js do
sign_in(admin)
gitlab_enable_admin_mode_sign_in(admin)
visit new_project_path
find('[data-qa-selector="blank_project_link"]').click
expect_custom_new_project_appearance(appearance)
end
......
......@@ -25,8 +25,8 @@ RSpec.describe 'New project', :js do
expect(page).to have_no_selector('a', text: 'New project/repository')
end
expect(page).to have_selector('.blank-state-title', text: 'Create blank project')
expect(page).to have_no_selector('.blank-state-title', text: 'Create blank project/repository')
expect(page).to have_selector('h3', text: 'Create blank project')
expect(page).to have_no_selector('h3', text: 'Create blank project/repository')
end
it 'when in candidate renders "project/repository"' do
......@@ -40,7 +40,7 @@ RSpec.describe 'New project', :js do
expect(page).to have_selector('a', text: 'New project/repository')
end
expect(page).to have_selector('.blank-state-title', text: 'Create blank project/repository')
expect(page).to have_selector('h3', text: 'Create blank project/repository')
end
context 'with combined_menu feature disabled' do
......@@ -240,7 +240,7 @@ RSpec.describe 'New project', :js do
find('[data-qa-selector="import_project_link"]').click
first('.js-import-git-toggle-button').click
page.within '.toggle-import-form' do
page.within '#import-project-pane' do
expect(page).not_to have_css('input#project_initialize_with_readme')
expect(page).not_to have_content('Initialize repository with a README')
end
......
import { shallowMount } from '@vue/test-utils';
import { assignGitlabExperiment } from 'helpers/experimentation_helper';
import App from '~/pages/projects/new/components/app.vue';
import NewNamespacePage from '~/vue_shared/new_namespace/new_namespace_page.vue';
describe('Experimental new project creation app', () => {
let wrapper;
const findNewNamespacePage = () => wrapper.findComponent(NewNamespacePage);
const createComponent = (propsData) => {
wrapper = shallowMount(App, { propsData });
};
afterEach(() => {
wrapper.destroy();
});
describe('new_repo experiment', () => {
it('passes new_repo experiment', () => {
createComponent();
expect(findNewNamespacePage().props().experiment).toBe('new_repo');
});
describe('when in the candidate variant', () => {
assignGitlabExperiment('new_repo', 'candidate');
it('has "repository" in the panel title', () => {
createComponent();
expect(findNewNamespacePage().props().panels[0].title).toBe(
'Create blank project/repository',
);
});
});
describe('when in the control variant', () => {
assignGitlabExperiment('new_repo', 'control');
it('has "project" in the panel title', () => {
createComponent();
expect(findNewNamespacePage().props().panels[0].title).toBe('Create blank project');
});
});
});
it('passes custom new project guideline text to underlying component', () => {
const DEMO_GUIDELINES = 'Demo guidelines';
const guidelineSelector = '#new-project-guideline';
createComponent({
newProjectGuidelines: DEMO_GUIDELINES,
});
expect(wrapper.find(guidelineSelector).text()).toBe(DEMO_GUIDELINES);
});
it.each`
isCiCdAvailable | outcome
${false} | ${'do not show CI/CD panel'}
${true} | ${'show CI/CD panel'}
`('$outcome when isCiCdAvailable is $isCiCdAvailable', ({ isCiCdAvailable }) => {
createComponent({
isCiCdAvailable,
});
expect(
Boolean(
wrapper
.findComponent(NewNamespacePage)
.props()
.panels.find((p) => p.name === 'cicd_for_external_repo'),
),
).toBe(isCiCdAvailable);
});
});
import { GlPopover, GlFormInputGroup } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import NewProjectPushTipPopover from '~/projects/experiment_new_project_creation/components/new_project_push_tip_popover.vue';
import NewProjectPushTipPopover from '~/pages/projects/new/components/new_project_push_tip_popover.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
describe('New project push tip popover', () => {
......
import { GlBreadcrumb } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { assignGitlabExperiment } from 'helpers/experimentation_helper';
import App from '~/projects/experiment_new_project_creation/components/app.vue';
import LegacyContainer from '~/projects/experiment_new_project_creation/components/legacy_container.vue';
import WelcomePage from '~/projects/experiment_new_project_creation/components/welcome.vue';
describe('Experimental new project creation app', () => {
let wrapper;
const createComponent = (propsData) => {
wrapper = shallowMount(App, { propsData });
};
afterEach(() => {
wrapper.destroy();
window.location.hash = '';
wrapper = null;
});
const findWelcomePage = () => wrapper.findComponent(WelcomePage);
const findPanel = (panelName) =>
findWelcomePage()
.props()
.panels.find((p) => p.name === panelName);
const findPanelHeader = () => wrapper.find('h4');
describe('new_repo experiment', () => {
describe('when in the candidate variant', () => {
assignGitlabExperiment('new_repo', 'candidate');
it('has "repository" in the panel title', () => {
createComponent();
expect(findPanel('blank_project').title).toBe('Create blank project/repository');
});
describe('when hash is not empty on load', () => {
beforeEach(() => {
window.location.hash = '#blank_project';
createComponent();
});
it('renders "project/repository"', () => {
expect(findPanelHeader().text()).toBe('Create blank project/repository');
});
});
});
describe('when in the control variant', () => {
assignGitlabExperiment('new_repo', 'control');
it('has "project" in the panel title', () => {
createComponent();
expect(findPanel('blank_project').title).toBe('Create blank project');
});
describe('when hash is not empty on load', () => {
beforeEach(() => {
window.location.hash = '#blank_project';
createComponent();
});
it('renders "project"', () => {
expect(findPanelHeader().text()).toBe('Create blank project');
});
});
});
});
describe('with empty hash', () => {
beforeEach(() => {
createComponent();
});
it('renders welcome page', () => {
expect(wrapper.find(WelcomePage).exists()).toBe(true);
});
it('does not render breadcrumbs', () => {
expect(wrapper.find(GlBreadcrumb).exists()).toBe(false);
});
});
it('renders blank project container if there are errors', () => {
createComponent({ hasErrors: true });
expect(wrapper.find(WelcomePage).exists()).toBe(false);
expect(wrapper.find(LegacyContainer).exists()).toBe(true);
});
describe('when hash is not empty on load', () => {
beforeEach(() => {
window.location.hash = '#blank_project';
createComponent();
});
it('renders relevant container', () => {
expect(wrapper.find(WelcomePage).exists()).toBe(false);
expect(wrapper.find(LegacyContainer).exists()).toBe(true);
});
it('renders breadcrumbs', () => {
expect(wrapper.find(GlBreadcrumb).exists()).toBe(true);
});
});
describe('display custom new project guideline text', () => {
beforeEach(() => {
window.location.hash = '#blank_project';
});
it('does not render new project guideline if undefined', () => {
createComponent();
expect(wrapper.find('div#new-project-guideline').exists()).toBe(false);
});
it('render new project guideline if defined', () => {
const guidelineSelector = 'div#new-project-guideline';
createComponent({
newProjectGuidelines: '<h4>Internal Guidelines</h4><p>lorem ipsum</p>',
});
expect(wrapper.find(guidelineSelector).exists()).toBe(true);
expect(wrapper.find(guidelineSelector).html()).toContain('<h4>Internal Guidelines</h4>');
expect(wrapper.find(guidelineSelector).html()).toContain('<p>lorem ipsum</p>');
});
});
it('renders relevant container when hash changes', () => {
createComponent();
expect(wrapper.find(WelcomePage).exists()).toBe(true);
window.location.hash = '#blank_project';
const ev = document.createEvent('HTMLEvents');
ev.initEvent('hashchange', false, false);
window.dispatchEvent(ev);
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.find(WelcomePage).exists()).toBe(false);
expect(wrapper.find(LegacyContainer).exists()).toBe(true);
});
});
});
import { shallowMount } from '@vue/test-utils';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import LegacyContainer from '~/projects/experiment_new_project_creation/components/legacy_container.vue';
import LegacyContainer from '~/vue_shared/new_namespace/components/legacy_container.vue';
describe('Legacy container component', () => {
let wrapper;
......
......@@ -3,8 +3,7 @@ import { nextTick } from 'vue';
import { mockTracking } from 'helpers/tracking_helper';
import { TRACKING_CONTEXT_SCHEMA } from '~/experimentation/constants';
import { getExperimentData } from '~/experimentation/utils';
import NewProjectPushTipPopover from '~/projects/experiment_new_project_creation/components/new_project_push_tip_popover.vue';
import WelcomePage from '~/projects/experiment_new_project_creation/components/welcome.vue';
import WelcomePage from '~/vue_shared/new_namespace/components/welcome.vue';
jest.mock('~/experimentation/utils', () => ({ getExperimentData: jest.fn() }));
......@@ -12,8 +11,18 @@ describe('Welcome page', () => {
let wrapper;
let trackingSpy;
const createComponent = (propsData) => {
wrapper = shallowMount(WelcomePage, { propsData });
const DEFAULT_PROPS = {
title: 'Create new something',
};
const createComponent = ({ propsData, slots }) => {
wrapper = shallowMount(WelcomePage, {
slots,
propsData: {
...DEFAULT_PROPS,
...propsData,
},
});
};
beforeEach(() => {
......@@ -29,7 +38,7 @@ describe('Welcome page', () => {
});
it('tracks link clicks', async () => {
createComponent({ panels: [{ name: 'test', href: '#' }] });
createComponent({ propsData: { experiment: 'foo', panels: [{ name: 'test', href: '#' }] } });
const link = wrapper.find('a');
link.trigger('click');
await nextTick();
......@@ -38,11 +47,11 @@ describe('Welcome page', () => {
});
});
it('adds new_repo experiment data if in experiment', async () => {
it('adds experiment data if in experiment', async () => {
const mockExperimentData = 'data';
getExperimentData.mockReturnValue(mockExperimentData);
createComponent({ panels: [{ name: 'test', href: '#' }] });
createComponent({ propsData: { experiment: 'foo', panels: [{ name: 'test', href: '#' }] } });
const link = wrapper.find('a');
link.trigger('click');
await nextTick();
......@@ -57,12 +66,13 @@ describe('Welcome page', () => {
});
});
it('renders new project push tip popover', () => {
createComponent({ panels: [{ name: 'test', href: '#' }] });
const popover = wrapper.findComponent(NewProjectPushTipPopover);
it('renders footer slot if provided', () => {
const DUMMY = 'Test message';
createComponent({
slots: { footer: DUMMY },
propsData: { panels: [{ name: 'test', href: '#' }] },
});
expect(popover.exists()).toBe(true);
expect(popover.props().target()).toBe(wrapper.find({ ref: 'clipTip' }).element);
expect(wrapper.text()).toContain(DUMMY);
});
});
import { GlBreadcrumb } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import LegacyContainer from '~/vue_shared/new_namespace/components/legacy_container.vue';
import WelcomePage from '~/vue_shared/new_namespace/components/welcome.vue';
import NewNamespacePage from '~/vue_shared/new_namespace/new_namespace_page.vue';
describe('Experimental new project creation app', () => {
let wrapper;
const findWelcomePage = () => wrapper.findComponent(WelcomePage);
const findLegacyContainer = () => wrapper.findComponent(LegacyContainer);
const findBreadcrumb = () => wrapper.findComponent(GlBreadcrumb);
const DEFAULT_PROPS = {
title: 'Create something',
initialBreadcrumb: 'Something',
panels: [
{ name: 'panel1', selector: '#some-selector1' },
{ name: 'panel2', selector: '#some-selector2' },
],
persistenceKey: 'DEMO-PERSISTENCE-KEY',
};
const createComponent = ({ slots, propsData } = {}) => {
wrapper = shallowMount(NewNamespacePage, {
slots,
propsData: {
...DEFAULT_PROPS,
...propsData,
},
});
};
afterEach(() => {
wrapper.destroy();
window.location.hash = '';
});
it('passes experiment to welcome component if provided', () => {
const EXPERIMENT = 'foo';
createComponent({ propsData: { experiment: EXPERIMENT } });
expect(findWelcomePage().props().experiment).toBe(EXPERIMENT);
});
describe('with empty hash', () => {
beforeEach(() => {
createComponent();
});
it('renders welcome page', () => {
expect(findWelcomePage().exists()).toBe(true);
});
it('does not render breadcrumbs', () => {
expect(findBreadcrumb().exists()).toBe(false);
});
});
it('renders first container if jumpToLastPersistedPanel passed', () => {
createComponent({ propsData: { jumpToLastPersistedPanel: true } });
expect(findWelcomePage().exists()).toBe(false);
expect(findLegacyContainer().exists()).toBe(true);
});
describe('when hash is not empty on load', () => {
beforeEach(() => {
window.location.hash = `#${DEFAULT_PROPS.panels[1].name}`;
createComponent();
});
it('renders relevant container', () => {
expect(findWelcomePage().exists()).toBe(false);
const container = findLegacyContainer();
expect(container.exists()).toBe(true);
expect(container.props().selector).toBe(DEFAULT_PROPS.panels[1].selector);
});
it('renders breadcrumbs', () => {
const breadcrumb = findBreadcrumb();
expect(breadcrumb.exists()).toBe(true);
expect(breadcrumb.props().items[0].text).toBe(DEFAULT_PROPS.initialBreadcrumb);
});
});
it('renders extra description if provided', () => {
window.location.hash = `#${DEFAULT_PROPS.panels[1].name}`;
const EXTRA_DESCRIPTION = 'Some extra description';
createComponent({
slots: {
'extra-description': EXTRA_DESCRIPTION,
},
});
expect(wrapper.text()).toContain(EXTRA_DESCRIPTION);
});
it('renders relevant container when hash changes', async () => {
createComponent();
expect(findWelcomePage().exists()).toBe(true);
window.location.hash = `#${DEFAULT_PROPS.panels[0].name}`;
const ev = document.createEvent('HTMLEvents');
ev.initEvent('hashchange', false, false);
window.dispatchEvent(ev);
await nextTick();
expect(findWelcomePage().exists()).toBe(false);
expect(findLegacyContainer().exists()).toBe(true);
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment