diff options
Diffstat (limited to 'app/assets/javascripts/projects/new')
6 files changed, 436 insertions, 0 deletions
diff --git a/app/assets/javascripts/projects/new/components/app.vue b/app/assets/javascripts/projects/new/components/app.vue new file mode 100644 index 00000000000..6e9efc50be8 --- /dev/null +++ b/app/assets/javascripts/projects/new/components/app.vue @@ -0,0 +1,124 @@ +<script> +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 { s__ } from '~/locale'; +import NewNamespacePage from '~/vue_shared/new_namespace/new_namespace_page.vue'; +import NewProjectPushTipPopover from './new_project_push_tip_popover.vue'; + +const CI_CD_PANEL = 'cicd_for_external_repo'; +const PANELS = [ + { + key: 'blank', + name: 'blank_project', + selector: '#blank-project-pane', + title: s__('ProjectsNew|Create blank project'), + description: s__( + 'ProjectsNew|Create a blank project to house your files, plan your work, and collaborate on code, among other things.', + ), + illustration: blankProjectIllustration, + }, + { + key: 'template', + name: 'create_from_template', + selector: '#create-from-template-pane', + title: s__('ProjectsNew|Create from template'), + description: s__( + 'ProjectsNew|Create a project pre-populated with the necessary files to get you started quickly.', + ), + illustration: createFromTemplateIllustration, + }, + { + key: 'import', + name: 'import_project', + selector: '#import-project-pane', + title: s__('ProjectsNew|Import project'), + description: s__( + 'ProjectsNew|Migrate your data from an external source like GitHub, Bitbucket, or another instance of GitLab.', + ), + illustration: importProjectIllustration, + }, + { + key: 'ci', + name: CI_CD_PANEL, + selector: '#ci-cd-project-pane', + title: s__('ProjectsNew|Run CI/CD for external repository'), + description: s__('ProjectsNew|Connect your external repository to GitLab CI/CD.'), + illustration: ciCdProjectIllustration, + }, +]; + +export default { + components: { + NewNamespacePage, + NewProjectPushTipPopover, + }, + directives: { + SafeHtml, + }, + props: { + hasErrors: { + type: Boolean, + required: false, + default: false, + }, + isCiCdAvailable: { + type: Boolean, + required: false, + default: false, + }, + newProjectGuidelines: { + type: String, + required: false, + default: '', + }, + }, + + computed: { + availablePanels() { + return this.isCiCdAvailable ? PANELS : PANELS.filter((p) => p.name !== CI_CD_PANEL); + }, + }, + + methods: { + resetProjectErrors() { + const errorsContainer = document.querySelector('.project-edit-errors'); + if (errorsContainer) { + errorsContainer.innerHTML = ''; + } + }, + }, +}; +</script> + +<template> + <new-namespace-page + :initial-breadcrumb="s__('New project')" + :panels="availablePanels" + :jump-to-last-persisted-panel="hasErrors" + :title="s__('ProjectsNew|Create new project')" + 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> + </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> diff --git a/app/assets/javascripts/projects/new/components/new_project_push_tip_popover.vue b/app/assets/javascripts/projects/new/components/new_project_push_tip_popover.vue new file mode 100644 index 00000000000..e42d9154866 --- /dev/null +++ b/app/assets/javascripts/projects/new/components/new_project_push_tip_popover.vue @@ -0,0 +1,66 @@ +<script> +import { GlPopover, GlFormInputGroup } from '@gitlab/ui'; +import { __ } from '~/locale'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; + +export default { + components: { + GlPopover, + GlFormInputGroup, + ClipboardButton, + }, + inject: ['pushToCreateProjectCommand', 'workingWithProjectsHelpPath'], + props: { + target: { + type: [Function, HTMLElement], + required: true, + }, + }, + i18n: { + clipboardButtonTitle: __('Copy command'), + commandInputAriaLabel: __('Push project from command line'), + helpLinkText: __('What does this command do?'), + labelText: __('Private projects can be created in your personal namespace with:'), + popoverTitle: __('Push to create a project'), + }, +}; +</script> +<template> + <gl-popover + :target="target" + :title="$options.i18n.popoverTitle" + triggers="click blur" + placement="top" + > + <p> + <label for="push-to-create-tip" class="gl-font-weight-normal"> + {{ $options.i18n.labelText }} + </label> + </p> + <p> + <gl-form-input-group + id="push-to-create-tip" + :value="pushToCreateProjectCommand" + readonly + select-on-click + :aria-label="$options.i18n.commandInputAriaLabel" + > + <template #append> + <clipboard-button + :text="pushToCreateProjectCommand" + :title="$options.i18n.clipboardButtonTitle" + tooltip-placement="right" + /> + </template> + </gl-form-input-group> + </p> + <p> + <a + :href="`${workingWithProjectsHelpPath}#push-to-create-a-new-project`" + class="gl-font-sm" + target="_blank" + >{{ $options.i18n.helpLinkText }}</a + > + </p> + </gl-popover> +</template> diff --git a/app/assets/javascripts/projects/new/components/new_project_url_select.vue b/app/assets/javascripts/projects/new/components/new_project_url_select.vue new file mode 100644 index 00000000000..bf44ff70562 --- /dev/null +++ b/app/assets/javascripts/projects/new/components/new_project_url_select.vue @@ -0,0 +1,163 @@ +<script> +import { + GlButton, + GlButtonGroup, + GlDropdown, + GlDropdownItem, + GlDropdownText, + GlDropdownSectionHeader, + GlLoadingIcon, + GlSearchBoxByType, +} from '@gitlab/ui'; +import { MINIMUM_SEARCH_LENGTH } from '~/graphql_shared/constants'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import Tracking from '~/tracking'; +import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants'; +import searchNamespacesWhereUserCanCreateProjectsQuery from '../queries/search_namespaces_where_user_can_create_projects.query.graphql'; +import eventHub from '../event_hub'; + +export default { + components: { + GlButton, + GlButtonGroup, + GlDropdown, + GlDropdownItem, + GlDropdownText, + GlDropdownSectionHeader, + GlLoadingIcon, + GlSearchBoxByType, + }, + mixins: [Tracking.mixin()], + apollo: { + currentUser: { + query: searchNamespacesWhereUserCanCreateProjectsQuery, + variables() { + return { + search: this.search, + }; + }, + skip() { + return this.search.length > 0 && this.search.length < MINIMUM_SEARCH_LENGTH; + }, + debounce: DEBOUNCE_DELAY, + }, + }, + inject: [ + 'namespaceFullPath', + 'namespaceId', + 'rootUrl', + 'trackLabel', + 'userNamespaceFullPath', + 'userNamespaceId', + ], + data() { + return { + currentUser: {}, + groupToFilterBy: undefined, + search: '', + selectedNamespace: this.namespaceId + ? { + id: this.namespaceId, + fullPath: this.namespaceFullPath, + } + : { + id: this.userNamespaceId, + fullPath: this.userNamespaceFullPath, + }, + }; + }, + computed: { + userGroups() { + return this.currentUser.groups?.nodes || []; + }, + userNamespace() { + return this.currentUser.namespace || {}; + }, + filteredGroups() { + return this.groupToFilterBy + ? this.userGroups.filter((group) => + group.fullPath.startsWith(this.groupToFilterBy.fullPath), + ) + : this.userGroups; + }, + hasGroupMatches() { + return this.filteredGroups.length; + }, + hasNamespaceMatches() { + return ( + this.userNamespace.fullPath?.toLowerCase().includes(this.search.toLowerCase()) && + !this.groupToFilterBy + ); + }, + hasNoMatches() { + return !this.hasGroupMatches && !this.hasNamespaceMatches; + }, + }, + created() { + eventHub.$on('select-template', this.handleSelectTemplate); + }, + beforeDestroy() { + eventHub.$off('select-template', this.handleSelectTemplate); + }, + methods: { + focusInput() { + this.$refs.search.focusInput(); + }, + handleSelectTemplate(groupId) { + this.groupToFilterBy = this.userGroups.find( + (group) => getIdFromGraphQLId(group.id) === groupId, + ); + if (this.groupToFilterBy) { + this.setNamespace(this.groupToFilterBy); + } + }, + setNamespace({ id, fullPath }) { + this.selectedNamespace = { + id: getIdFromGraphQLId(id), + fullPath, + }; + }, + }, +}; +</script> + +<template> + <gl-button-group class="input-lg"> + <gl-button class="gl-text-truncate" label :title="rootUrl">{{ rootUrl }}</gl-button> + <gl-dropdown + :text="selectedNamespace.fullPath" + toggle-class="gl-rounded-top-right-base! gl-rounded-bottom-right-base! gl-w-20" + data-qa-selector="select_namespace_dropdown" + @show="track('activate_form_input', { label: trackLabel, property: 'project_path' })" + @shown="focusInput" + > + <gl-search-box-by-type + ref="search" + v-model.trim="search" + data-qa-selector="select_namespace_dropdown_search_field" + /> + <gl-loading-icon v-if="$apollo.queries.currentUser.loading" /> + <template v-else> + <template v-if="hasGroupMatches"> + <gl-dropdown-section-header>{{ __('Groups') }}</gl-dropdown-section-header> + <gl-dropdown-item + v-for="group of filteredGroups" + :key="group.id" + @click="setNamespace(group)" + > + {{ group.fullPath }} + </gl-dropdown-item> + </template> + <template v-if="hasNamespaceMatches"> + <gl-dropdown-section-header>{{ __('Users') }}</gl-dropdown-section-header> + <gl-dropdown-item @click="setNamespace(userNamespace)"> + {{ userNamespace.fullPath }} + </gl-dropdown-item> + </template> + <gl-dropdown-text v-if="hasNoMatches">{{ __('No matches found') }}</gl-dropdown-text> + </template> + </gl-dropdown> + + <input type="hidden" name="project[namespace_id]" :value="selectedNamespace.id" /> + </gl-button-group> +</template> diff --git a/app/assets/javascripts/projects/new/event_hub.js b/app/assets/javascripts/projects/new/event_hub.js new file mode 100644 index 00000000000..e31806ad199 --- /dev/null +++ b/app/assets/javascripts/projects/new/event_hub.js @@ -0,0 +1,3 @@ +import createEventHub from '~/helpers/event_hub_factory'; + +export default createEventHub(); diff --git a/app/assets/javascripts/projects/new/index.js b/app/assets/javascripts/projects/new/index.js new file mode 100644 index 00000000000..572d3276e4f --- /dev/null +++ b/app/assets/javascripts/projects/new/index.js @@ -0,0 +1,66 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; +import { parseBoolean } from '~/lib/utils/common_utils'; +import NewProjectCreationApp from './components/app.vue'; +import NewProjectUrlSelect from './components/new_project_url_select.vue'; + +export function initNewProjectCreation() { + const el = document.querySelector('.js-new-project-creation'); + + const { + pushToCreateProjectCommand, + workingWithProjectsHelpPath, + newProjectGuidelines, + hasErrors, + isCiCdAvailable, + } = el.dataset; + + const props = { + hasErrors: parseBoolean(hasErrors), + isCiCdAvailable: parseBoolean(isCiCdAvailable), + newProjectGuidelines, + }; + + const provide = { + workingWithProjectsHelpPath, + pushToCreateProjectCommand, + }; + + return new Vue({ + el, + provide, + render(h) { + return h(NewProjectCreationApp, { props }); + }, + }); +} + +export function initNewProjectUrlSelect() { + const elements = document.querySelectorAll('.js-vue-new-project-url-select'); + + if (!elements.length) { + return; + } + + Vue.use(VueApollo); + + elements.forEach( + (el) => + new Vue({ + el, + apolloProvider: new VueApollo({ + defaultClient: createDefaultClient({}, { assumeImmutableResults: true }), + }), + provide: { + namespaceFullPath: el.dataset.namespaceFullPath, + namespaceId: el.dataset.namespaceId, + rootUrl: el.dataset.rootUrl, + trackLabel: el.dataset.trackLabel, + userNamespaceFullPath: el.dataset.userNamespaceFullPath, + userNamespaceId: el.dataset.userNamespaceId, + }, + render: (createElement) => createElement(NewProjectUrlSelect), + }), + ); +} diff --git a/app/assets/javascripts/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql b/app/assets/javascripts/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql new file mode 100644 index 00000000000..e16fe5dde49 --- /dev/null +++ b/app/assets/javascripts/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql @@ -0,0 +1,14 @@ +query searchNamespacesWhereUserCanCreateProjects($search: String) { + currentUser { + groups(permissionScope: CREATE_PROJECTS, search: $search) { + nodes { + id + fullPath + } + } + namespace { + id + fullPath + } + } +} |