mirror of
https://github.com/gitlabhq/gitlabhq.git
synced 2025-07-20 16:42:55 +00:00
Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
@ -95,23 +95,6 @@ export default {
|
||||
'app/assets/javascripts/google_cloud/gcp_regions/list.vue',
|
||||
'app/assets/javascripts/groups/components/group_item.vue',
|
||||
'app/assets/javascripts/groups/components/invite_members_banner.vue',
|
||||
'app/assets/javascripts/ide/components/commit_sidebar/form.vue',
|
||||
'app/assets/javascripts/ide/components/commit_sidebar/list_item.vue',
|
||||
'app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue',
|
||||
'app/assets/javascripts/ide/components/file_row_extra.vue',
|
||||
'app/assets/javascripts/ide/components/ide.vue',
|
||||
'app/assets/javascripts/ide/components/ide_side_bar.vue',
|
||||
'app/assets/javascripts/ide/components/ide_status_bar.vue',
|
||||
'app/assets/javascripts/ide/components/ide_tree.vue',
|
||||
'app/assets/javascripts/ide/components/ide_tree_list.vue',
|
||||
'app/assets/javascripts/ide/components/jobs/item.vue',
|
||||
'app/assets/javascripts/ide/components/merge_requests/list.vue',
|
||||
'app/assets/javascripts/ide/components/new_dropdown/modal.vue',
|
||||
'app/assets/javascripts/ide/components/panes/right.vue',
|
||||
'app/assets/javascripts/ide/components/pipelines/list.vue',
|
||||
'app/assets/javascripts/ide/components/repo_commit_section.vue',
|
||||
'app/assets/javascripts/ide/components/repo_editor.vue',
|
||||
'app/assets/javascripts/ide/components/repo_tabs.vue',
|
||||
'app/assets/javascripts/import_entities/import_groups/components/import_actions_cell.vue',
|
||||
'app/assets/javascripts/import_entities/import_groups/components/import_table.vue',
|
||||
'app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue',
|
||||
|
@ -1251,6 +1251,7 @@ lib/gitlab/checks/**
|
||||
# i18n Docs
|
||||
[Localization Team] @gitlab-com/localization/maintainers
|
||||
/doc-locale/**
|
||||
/doc/development/i18n/proofreader.md
|
||||
|
||||
[Authorization] @gitlab-org/software-supply-chain-security/authorization/approvers
|
||||
/config/initializers/declarative_policy.rb
|
||||
|
@ -21,8 +21,7 @@
|
||||
{
|
||||
"files": [
|
||||
"app/assets/stylesheets/application_dark.scss",
|
||||
"app/assets/stylesheets/framework/**/*.scss",
|
||||
"app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss"
|
||||
"app/assets/stylesheets/framework/**/*.scss"
|
||||
],
|
||||
"rules": {
|
||||
"gitlab/no-gl-class": null
|
||||
|
@ -1 +1 @@
|
||||
c9a38829a0c35e0d325941304873f715fb10507e
|
||||
b8a5f8c7440eb511a10903d7f3d1862c96cd6428
|
||||
|
@ -163,7 +163,7 @@ export default {
|
||||
:title="restProjectsTooltip"
|
||||
class="deploy-project-label gl-mb-2 gl-mr-2 gl-truncate"
|
||||
href="#"
|
||||
@click.native="toggleExpanded"
|
||||
@click="toggleExpanded"
|
||||
>
|
||||
<span class="gl-truncate">{{ restProjectsLabel }}</span>
|
||||
</gl-badge>
|
||||
|
@ -1,178 +0,0 @@
|
||||
/**
|
||||
* A WebIDE Extension options for Source Editor
|
||||
* @typedef {Object} WebIDEExtensionOptions
|
||||
* @property {Object} modelManager The root manager for WebIDE models
|
||||
* @property {Object} store The state store for communication
|
||||
* @property {Object} file
|
||||
* @property {Object} options The Monaco editor options
|
||||
*/
|
||||
|
||||
import { KeyCode, KeyMod, Range } from 'monaco-editor';
|
||||
import { EDITOR_TYPE_DIFF } from '~/editor/constants';
|
||||
import Disposable from '~/ide/lib/common/disposable';
|
||||
import { editorOptions } from '~/ide/lib/editor_options';
|
||||
import keymap from '~/ide/lib/keymap.json';
|
||||
|
||||
const isDiffEditorType = (instance) => {
|
||||
return instance.getEditorType() === EDITOR_TYPE_DIFF;
|
||||
};
|
||||
|
||||
export const UPDATE_DIMENSIONS_DELAY = 200;
|
||||
const defaultOptions = {
|
||||
modelManager: undefined,
|
||||
store: undefined,
|
||||
file: undefined,
|
||||
options: {},
|
||||
};
|
||||
|
||||
const addActions = (instance, store) => {
|
||||
const getKeyCode = (key) => {
|
||||
const monacoKeyMod = key.indexOf('KEY_') === 0;
|
||||
|
||||
return monacoKeyMod ? KeyCode[key] : KeyMod[key];
|
||||
};
|
||||
|
||||
keymap.forEach((command) => {
|
||||
const { bindings, id, label, action } = command;
|
||||
|
||||
const keybindings = bindings.map((binding) => {
|
||||
const keys = binding.split('+');
|
||||
|
||||
// eslint-disable-next-line no-bitwise
|
||||
return keys.length > 1 ? getKeyCode(keys[0]) | getKeyCode(keys[1]) : getKeyCode(keys[0]);
|
||||
});
|
||||
|
||||
instance.addAction({
|
||||
id,
|
||||
label,
|
||||
keybindings,
|
||||
run() {
|
||||
store.dispatch(action.name, action.params);
|
||||
return null;
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const renderSideBySide = (domElement) => {
|
||||
return domElement.offsetWidth >= 700;
|
||||
};
|
||||
|
||||
const updateDiffInstanceRendering = (instance) => {
|
||||
instance.updateOptions({
|
||||
renderSideBySide: renderSideBySide(instance.getDomNode()),
|
||||
});
|
||||
};
|
||||
|
||||
export class EditorWebIdeExtension {
|
||||
static get extensionName() {
|
||||
return 'EditorWebIde';
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the WebIDE extension for Source Editor
|
||||
* @param {module:source_editor_instance~EditorInstance} instance - The Source Editor instance
|
||||
* @param {WebIDEExtensionOptions} setupOptions
|
||||
*/
|
||||
onSetup(instance, setupOptions = defaultOptions) {
|
||||
this.modelManager = setupOptions.modelManager;
|
||||
this.store = setupOptions.store;
|
||||
this.file = setupOptions.file;
|
||||
this.options = setupOptions.options;
|
||||
|
||||
this.disposable = new Disposable();
|
||||
addActions(instance, setupOptions.store);
|
||||
|
||||
if (isDiffEditorType(instance)) {
|
||||
updateDiffInstanceRendering(instance);
|
||||
instance.getModifiedEditor().onDidLayoutChange(() => {
|
||||
updateDiffInstanceRendering(instance);
|
||||
});
|
||||
}
|
||||
|
||||
instance.onDidDispose(() => {
|
||||
this.onUnuse();
|
||||
});
|
||||
}
|
||||
|
||||
onUnuse() {
|
||||
// catch any potential errors with disposing the error
|
||||
// this is mainly for tests caused by elements not existing
|
||||
try {
|
||||
this.disposable.dispose();
|
||||
} catch (e) {
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provides() {
|
||||
return {
|
||||
createModel: (instance, file, head = null) => {
|
||||
return this.modelManager.addModel(file, head);
|
||||
},
|
||||
attachModel: (instance, model) => {
|
||||
if (isDiffEditorType(instance)) {
|
||||
instance.setModel({
|
||||
original: model.getOriginalModel(),
|
||||
modified: model.getModel(),
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
instance.setModel(model.getModel());
|
||||
|
||||
instance.updateOptions(
|
||||
editorOptions.reduce((acc, obj) => {
|
||||
Object.keys(obj).forEach((key) => {
|
||||
Object.assign(acc, {
|
||||
[key]: obj[key](model),
|
||||
});
|
||||
});
|
||||
return acc;
|
||||
}, {}),
|
||||
);
|
||||
},
|
||||
attachMergeRequestModel: (instance, model) => {
|
||||
instance.setModel({
|
||||
original: model.getBaseModel(),
|
||||
modified: model.getModel(),
|
||||
});
|
||||
},
|
||||
setPos: (instance, { lineNumber, column }) => {
|
||||
instance.revealPositionInCenter({
|
||||
lineNumber,
|
||||
column,
|
||||
});
|
||||
instance.setPosition({
|
||||
lineNumber,
|
||||
column,
|
||||
});
|
||||
},
|
||||
onPositionChange: (instance, cb) => {
|
||||
if (typeof instance.onDidChangeCursorPosition !== 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.disposable.add(instance.onDidChangeCursorPosition((e) => cb(instance, e)));
|
||||
},
|
||||
replaceSelectedText: (instance, text) => {
|
||||
let selection = instance.getSelection();
|
||||
const range = new Range(
|
||||
selection.startLineNumber,
|
||||
selection.startColumn,
|
||||
selection.endLineNumber,
|
||||
selection.endColumn,
|
||||
);
|
||||
|
||||
instance.executeEdits('', [{ range, text }]);
|
||||
|
||||
selection = instance.getSelection();
|
||||
instance.setPosition({ lineNumber: selection.endLineNumber, column: selection.endColumn });
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
@ -89,7 +89,7 @@ export default {
|
||||
:icon="statusIcon"
|
||||
:href="badgeHref"
|
||||
tabindex="0"
|
||||
@click.native="onClick"
|
||||
@click="onClick"
|
||||
>
|
||||
{{ badgeText }}
|
||||
</gl-badge>
|
||||
|
@ -256,7 +256,7 @@ export default {
|
||||
data-testid="sync-badge"
|
||||
tabindex="0"
|
||||
:href="fluxBadgeHref"
|
||||
@click.native="toggleFluxResource('')"
|
||||
@click="toggleFluxResource('')"
|
||||
>{{ syncStatusBadge.text }}
|
||||
<gl-popover :target="fluxBadgeId" :title="syncStatusBadge.popoverTitle">
|
||||
<span
|
||||
|
@ -65,7 +65,7 @@ export default {
|
||||
class="gl-align-middle"
|
||||
:variant="status"
|
||||
data-testid="check-version-badge"
|
||||
@click.native="onClick"
|
||||
@click="onClick"
|
||||
>{{ title }}</gl-badge
|
||||
>
|
||||
</template>
|
||||
|
@ -1,98 +0,0 @@
|
||||
<script>
|
||||
import { GlIcon, GlTooltipDirective, GlBadge } from '@gitlab/ui';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapActions, mapState } from 'vuex';
|
||||
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
|
||||
import { leftSidebarViews } from '../constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlIcon,
|
||||
GlBadge,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
computed: {
|
||||
...mapState(['currentActivityView', 'stagedFiles']),
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['updateActivityBarView']),
|
||||
changedActivityView(e, view) {
|
||||
e.currentTarget.blur();
|
||||
|
||||
this.updateActivityBarView(view);
|
||||
|
||||
this.$root.$emit(BV_HIDE_TOOLTIP);
|
||||
},
|
||||
},
|
||||
leftSidebarViews,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav class="ide-activity-bar" data-testid="left-sidebar">
|
||||
<ul class="list-unstyled">
|
||||
<li>
|
||||
<button
|
||||
v-gl-tooltip.right.viewport
|
||||
:class="{
|
||||
active: currentActivityView === $options.leftSidebarViews.edit.name,
|
||||
}"
|
||||
:title="s__('IDE|Edit')"
|
||||
:aria-label="s__('IDE|Edit')"
|
||||
data-container="body"
|
||||
data-placement="right"
|
||||
data-testid="edit-mode-button"
|
||||
type="button"
|
||||
class="ide-sidebar-link js-ide-edit-mode"
|
||||
@click.prevent="changedActivityView($event, $options.leftSidebarViews.edit.name)"
|
||||
>
|
||||
<gl-icon name="code" />
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
v-gl-tooltip.right.viewport
|
||||
:class="{
|
||||
active: currentActivityView === $options.leftSidebarViews.review.name,
|
||||
}"
|
||||
:title="s__('IDE|Review')"
|
||||
:aria-label="s__('IDE|Review')"
|
||||
data-container="body"
|
||||
data-placement="right"
|
||||
data-testid="review-mode-button"
|
||||
type="button"
|
||||
class="ide-sidebar-link js-ide-review-mode"
|
||||
@click.prevent="changedActivityView($event, $options.leftSidebarViews.review.name)"
|
||||
>
|
||||
<gl-icon name="review-list" />
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
v-gl-tooltip.right.viewport
|
||||
:class="{
|
||||
active: currentActivityView === $options.leftSidebarViews.commit.name,
|
||||
}"
|
||||
:title="s__('IDE|Commit')"
|
||||
:aria-label="s__('IDE|Commit')"
|
||||
data-container="body"
|
||||
data-placement="right"
|
||||
data-testid="commit-mode-button"
|
||||
type="button"
|
||||
class="ide-sidebar-link js-ide-commit-mode"
|
||||
@click.prevent="changedActivityView($event, $options.leftSidebarViews.commit.name)"
|
||||
>
|
||||
<gl-icon name="commit" />
|
||||
<gl-badge
|
||||
v-if="stagedFiles.length"
|
||||
class="gl-absolute gl-right-3 gl-top-3 !gl-bg-gray-900 gl-px-2 gl-font-bold !gl-text-white"
|
||||
>
|
||||
{{ stagedFiles.length }}
|
||||
</gl-badge>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</template>
|
@ -1,50 +0,0 @@
|
||||
<!-- eslint-disable vue/multi-word-component-names -->
|
||||
<script>
|
||||
/* eslint-disable @gitlab/vue-require-i18n-strings */
|
||||
import { GlIcon, GlButton } from '@gitlab/ui';
|
||||
import Timeago from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlIcon,
|
||||
Timeago,
|
||||
GlButton,
|
||||
},
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
projectId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
branchHref() {
|
||||
return this.$router.resolve(`/project/${this.projectId}/edit/${this.item.name}`).href;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-button variant="link" :href="branchHref">
|
||||
<span class="gl-flex gl-items-center">
|
||||
<span class="ide-search-list-current-icon gl-mr-3 gl-flex">
|
||||
<gl-icon v-if="isActive" :size="16" name="mobile-issue-close" />
|
||||
</span>
|
||||
<span class="gl-flex gl-flex-col gl-items-end">
|
||||
<strong> {{ item.name }} </strong>
|
||||
<span class="ide-merge-request-project-path mt-1 gl-block">
|
||||
Updated <timeago :time="item.committedDate || ''" />
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</gl-button>
|
||||
</template>
|
@ -1,95 +0,0 @@
|
||||
<script>
|
||||
import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
|
||||
import { debounce } from 'lodash';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapActions, mapState } from 'vuex';
|
||||
import Item from './item.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Item,
|
||||
GlIcon,
|
||||
GlLoadingIcon,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
search: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState('branches', ['branches', 'isLoading']),
|
||||
...mapState(['currentBranchId', 'currentProjectId']),
|
||||
hasBranches() {
|
||||
return this.branches.length !== 0;
|
||||
},
|
||||
hasNoSearchResults() {
|
||||
return this.search !== '' && !this.hasBranches;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
isLoading: {
|
||||
handler: 'focusSearch',
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.loadBranches();
|
||||
},
|
||||
methods: {
|
||||
...mapActions('branches', ['fetchBranches']),
|
||||
loadBranches() {
|
||||
this.fetchBranches({ search: this.search });
|
||||
},
|
||||
searchBranches: debounce(function debounceSearch() {
|
||||
this.loadBranches();
|
||||
}, 250),
|
||||
focusSearch() {
|
||||
if (!this.isLoading) {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.searchInput.focus();
|
||||
});
|
||||
}
|
||||
},
|
||||
isActiveBranch(item) {
|
||||
return item.name === this.currentBranchId;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<label
|
||||
class="dropdown-input gl-mb-0 gl-block gl-border-b-1 gl-pb-5 gl-pt-3 gl-border-b-solid"
|
||||
@click.stop
|
||||
>
|
||||
<input
|
||||
ref="searchInput"
|
||||
v-model="search"
|
||||
:placeholder="__('Search branches')"
|
||||
type="search"
|
||||
class="form-control dropdown-input-field"
|
||||
@input="searchBranches"
|
||||
/>
|
||||
<gl-icon name="search" class="input-icon gl-ml-5 gl-mt-1" />
|
||||
</label>
|
||||
<div class="dropdown-content ide-merge-requests-dropdown-content !gl-flex">
|
||||
<gl-loading-icon
|
||||
v-if="isLoading"
|
||||
size="lg"
|
||||
class="mt-3 mb-3 align-self-center ml-auto mr-auto"
|
||||
/>
|
||||
<ul v-else class="mb-0 gl-w-full">
|
||||
<template v-if="hasBranches">
|
||||
<li v-for="item in branches" :key="item.name">
|
||||
<item :item="item" :project-id="currentProjectId" :is-active="isActiveBranch(item)" />
|
||||
</li>
|
||||
</template>
|
||||
<li v-else class="ide-search-list-empty !gl-flex gl-items-center gl-justify-center">
|
||||
<template v-if="hasNoSearchResults">
|
||||
{{ __('No branches found') }}
|
||||
</template>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
@ -1,40 +0,0 @@
|
||||
<script>
|
||||
import { GlAlert, GlButton } from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlAlert,
|
||||
GlButton,
|
||||
},
|
||||
props: {
|
||||
message: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
action: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
hasAction() {
|
||||
return Boolean(this.action?.href);
|
||||
},
|
||||
actionButtonMethod() {
|
||||
return this.action?.isForm ? 'post' : null;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-alert :dismissible="false">
|
||||
{{ message }}
|
||||
<template v-if="hasAction" #actions>
|
||||
<gl-button variant="confirm" :href="action.href" :data-method="actionButtonMethod">
|
||||
{{ action.text }}
|
||||
</gl-button>
|
||||
</template>
|
||||
</gl-alert>
|
||||
</template>
|
@ -1,98 +0,0 @@
|
||||
<!-- eslint-disable vue/multi-word-component-names -->
|
||||
<script>
|
||||
import { GlSprintf } from '@gitlab/ui';
|
||||
import { escape } from 'lodash';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapState, mapGetters, createNamespacedHelpers } from 'vuex';
|
||||
import { s__ } from '~/locale';
|
||||
import {
|
||||
COMMIT_TO_CURRENT_BRANCH,
|
||||
COMMIT_TO_NEW_BRANCH,
|
||||
} from '../../stores/modules/commit/constants';
|
||||
import NewMergeRequestOption from './new_merge_request_option.vue';
|
||||
import RadioGroup from './radio_group.vue';
|
||||
|
||||
const { mapState: mapCommitState, mapActions: mapCommitActions } =
|
||||
createNamespacedHelpers('commit');
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlSprintf,
|
||||
RadioGroup,
|
||||
NewMergeRequestOption,
|
||||
},
|
||||
computed: {
|
||||
...mapState(['currentBranchId', 'changedFiles', 'stagedFiles']),
|
||||
...mapCommitState(['commitAction']),
|
||||
...mapGetters(['currentBranch', 'emptyRepo', 'canPushToBranch']),
|
||||
currentBranchText() {
|
||||
return escape(this.currentBranchId);
|
||||
},
|
||||
containsStagedChanges() {
|
||||
return this.changedFiles.length > 0 && this.stagedFiles.length > 0;
|
||||
},
|
||||
shouldDefaultToCurrentBranch() {
|
||||
if (this.emptyRepo) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.canPushToBranch && !this.currentBranch?.default;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
containsStagedChanges() {
|
||||
this.updateSelectedCommitAction();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (!this.commitAction) {
|
||||
this.updateSelectedCommitAction();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapCommitActions(['updateCommitAction']),
|
||||
updateSelectedCommitAction() {
|
||||
if (!this.currentBranch && !this.emptyRepo) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.shouldDefaultToCurrentBranch) {
|
||||
this.updateCommitAction(COMMIT_TO_CURRENT_BRANCH);
|
||||
} else {
|
||||
this.updateCommitAction(COMMIT_TO_NEW_BRANCH);
|
||||
}
|
||||
},
|
||||
},
|
||||
commitToCurrentBranch: COMMIT_TO_CURRENT_BRANCH,
|
||||
commitToNewBranch: COMMIT_TO_NEW_BRANCH,
|
||||
currentBranchPermissionsTooltip: s__(
|
||||
"IDE|This option is disabled because you don't have write permissions for the current branch.",
|
||||
),
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ide-commit-options gl-mb-5">
|
||||
<radio-group
|
||||
:value="$options.commitToCurrentBranch"
|
||||
:disabled="!canPushToBranch"
|
||||
:title="$options.currentBranchPermissionsTooltip"
|
||||
>
|
||||
<span class="ide-option-label">
|
||||
<gl-sprintf :message="s__('IDE|Commit to %{branchName} branch')">
|
||||
<template #branchName>
|
||||
<strong class="monospace">{{ currentBranchText }}</strong>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</span>
|
||||
</radio-group>
|
||||
<template v-if="!emptyRepo">
|
||||
<radio-group
|
||||
:value="$options.commitToNewBranch"
|
||||
:label="__('Create a new branch')"
|
||||
:show-input="true"
|
||||
/>
|
||||
<new-merge-request-option />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
@ -1,93 +0,0 @@
|
||||
<script>
|
||||
import { GlModal, GlButton } from '@gitlab/ui';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapActions } from 'vuex';
|
||||
import { sprintf, __ } from '~/locale';
|
||||
import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
|
||||
import FileIcon from '~/vue_shared/components/file_icon.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlModal,
|
||||
GlButton,
|
||||
FileIcon,
|
||||
ChangedFileIcon,
|
||||
},
|
||||
props: {
|
||||
activeFile: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
modal: {
|
||||
actionPrimary: {
|
||||
text: __('Discard changes'),
|
||||
attributes: {
|
||||
variant: 'danger',
|
||||
},
|
||||
},
|
||||
actionCancel: {
|
||||
text: __('Cancel'),
|
||||
attributes: {
|
||||
variant: 'default',
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
discardModalId() {
|
||||
return `discard-file-${this.activeFile.path}`;
|
||||
},
|
||||
discardModalTitle() {
|
||||
return sprintf(__('Discard changes to %{path}?'), { path: this.activeFile.path });
|
||||
},
|
||||
canDiscard() {
|
||||
return this.activeFile.changed || this.activeFile.staged;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['unstageChange', 'discardFileChanges']),
|
||||
showDiscardModal() {
|
||||
this.$refs.discardModal.show();
|
||||
},
|
||||
discardChanges(path) {
|
||||
this.unstageChange(path);
|
||||
this.discardFileChanges(path);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ide-commit-editor-header gl-flex gl-items-center">
|
||||
<file-icon :file-name="activeFile.name" :size="16" class="gl-mr-3" />
|
||||
<strong class="gl-mr-3">
|
||||
<template v-if="activeFile.prevPath && activeFile.prevPath !== activeFile.path">
|
||||
{{ activeFile.prevPath }} →
|
||||
</template>
|
||||
{{ activeFile.path }}
|
||||
</strong>
|
||||
<changed-file-icon :file="activeFile" :is-centered="false" />
|
||||
<div class="ml-auto">
|
||||
<gl-button
|
||||
v-if="canDiscard"
|
||||
ref="discardButton"
|
||||
category="secondary"
|
||||
variant="danger"
|
||||
class="gl-mr-3"
|
||||
@click="showDiscardModal"
|
||||
>
|
||||
{{ __('Discard changes') }}
|
||||
</gl-button>
|
||||
</div>
|
||||
<gl-modal
|
||||
ref="discardModal"
|
||||
:modal-id="discardModalId"
|
||||
:title="discardModalTitle"
|
||||
:action-primary="$options.modal.actionPrimary"
|
||||
:action-cancel="$options.modal.actionCancel"
|
||||
@primary="discardChanges(activeFile.path)"
|
||||
>
|
||||
{{ __("You will lose all changes you've made to this file. This action cannot be undone.") }}
|
||||
</gl-modal>
|
||||
</div>
|
||||
</template>
|
@ -1,24 +0,0 @@
|
||||
<script>
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapState } from 'vuex';
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
...mapState(['lastCommitMsg', 'noChangesStateSvgPath']),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="!lastCommitMsg" class="multi-file-commit-panel-section ide-commit-empty-state">
|
||||
<div class="ide-commit-empty-state-container">
|
||||
<div class="svg-content svg-80"><img :src="noChangesStateSvgPath" /></div>
|
||||
<div class="gl-ml-3 gl-mr-3">
|
||||
<div class="text-content text-center">
|
||||
<h4>{{ __('No changes') }}</h4>
|
||||
<p>{{ __('Edit files in the editor and commit changes here') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
@ -1,230 +0,0 @@
|
||||
<!-- eslint-disable vue/multi-word-component-names -->
|
||||
<script>
|
||||
import { GlModal, GlButton, GlTooltipDirective } from '@gitlab/ui';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapState, mapActions, mapGetters } from 'vuex';
|
||||
import SafeHtml from '~/vue_shared/directives/safe_html';
|
||||
import { n__ } from '~/locale';
|
||||
import { leftSidebarViews, MAX_WINDOW_HEIGHT_COMPACT } from '../../constants';
|
||||
import { createUnexpectedCommitError } from '../../lib/errors';
|
||||
import Actions from './actions.vue';
|
||||
import CommitMessageField from './message_field.vue';
|
||||
import SuccessMessage from './success_message.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Actions,
|
||||
CommitMessageField,
|
||||
SuccessMessage,
|
||||
GlModal,
|
||||
GlButton,
|
||||
},
|
||||
directives: {
|
||||
SafeHtml,
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isCompact: true,
|
||||
componentHeight: null,
|
||||
// Keep track of "lastCommitError" so we hold onto the value even when "commitError" is cleared.
|
||||
lastCommitError: createUnexpectedCommitError(),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(['changedFiles', 'stagedFiles', 'currentActivityView', 'lastCommitMsg']),
|
||||
...mapState('commit', ['commitMessage', 'submitCommitLoading', 'commitError']),
|
||||
...mapGetters(['someUncommittedChanges', 'canPushCodeStatus']),
|
||||
...mapGetters('commit', ['discardDraftButtonDisabled', 'preBuiltCommitMessage']),
|
||||
commitButtonDisabled() {
|
||||
return !this.canPushCodeStatus.isAllowed || !this.someUncommittedChanges;
|
||||
},
|
||||
commitButtonTooltip() {
|
||||
if (!this.canPushCodeStatus.isAllowed) {
|
||||
return this.canPushCodeStatus.messageShort;
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
overviewText() {
|
||||
return n__('%d changed file', '%d changed files', this.stagedFiles.length);
|
||||
},
|
||||
currentViewIsCommitView() {
|
||||
return this.currentActivityView === leftSidebarViews.commit.name;
|
||||
},
|
||||
commitErrorPrimaryAction() {
|
||||
const { primaryAction } = this.lastCommitError || {};
|
||||
|
||||
return {
|
||||
button: primaryAction ? { text: primaryAction.text } : undefined,
|
||||
callback: primaryAction?.callback?.bind(this, this.$store) || (() => {}),
|
||||
};
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentActivityView: 'handleCompactState',
|
||||
someUncommittedChanges: 'handleCompactState',
|
||||
lastCommitMsg: 'handleCompactState',
|
||||
commitError(val) {
|
||||
if (!val) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastCommitError = val;
|
||||
this.$refs.commitErrorModal.show();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['updateActivityBarView']),
|
||||
...mapActions('commit', [
|
||||
'updateCommitMessage',
|
||||
'discardDraft',
|
||||
'commitChanges',
|
||||
'updateCommitAction',
|
||||
]),
|
||||
commit() {
|
||||
// Even though the submit button will be disabled, we need to disable the submission
|
||||
// since hitting enter on the branch name text input also submits the form.
|
||||
if (!this.canPushCodeStatus.isAllowed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.commitChanges();
|
||||
},
|
||||
handleCompactState() {
|
||||
if (this.lastCommitMsg) {
|
||||
this.isCompact = false;
|
||||
} else {
|
||||
this.isCompact =
|
||||
!this.someUncommittedChanges ||
|
||||
!this.currentViewIsCommitView ||
|
||||
window.innerHeight < MAX_WINDOW_HEIGHT_COMPACT;
|
||||
}
|
||||
},
|
||||
toggleIsCompact() {
|
||||
this.isCompact = !this.isCompact;
|
||||
},
|
||||
beginCommit() {
|
||||
return this.updateActivityBarView(leftSidebarViews.commit.name).then(() => {
|
||||
this.isCompact = false;
|
||||
});
|
||||
},
|
||||
beforeEnterTransition() {
|
||||
const elHeight = this.isCompact
|
||||
? this.$refs.formEl && this.$refs.formEl.offsetHeight
|
||||
: this.$refs.compactEl && this.$refs.compactEl.offsetHeight;
|
||||
|
||||
this.componentHeight = elHeight;
|
||||
},
|
||||
enterTransition() {
|
||||
this.$nextTick(() => {
|
||||
const elHeight = this.isCompact
|
||||
? this.$refs.compactEl && this.$refs.compactEl.offsetHeight
|
||||
: this.$refs.formEl && this.$refs.formEl.offsetHeight;
|
||||
|
||||
this.componentHeight = elHeight;
|
||||
});
|
||||
},
|
||||
afterEndTransition() {
|
||||
this.componentHeight = null;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="{
|
||||
'is-compact': isCompact,
|
||||
'is-full': !isCompact,
|
||||
}"
|
||||
:style="{
|
||||
height: componentHeight ? `${componentHeight}px` : null,
|
||||
}"
|
||||
class="multi-file-commit-form"
|
||||
>
|
||||
<transition
|
||||
name="commit-form-slide-up"
|
||||
@before-enter="beforeEnterTransition"
|
||||
@enter="enterTransition"
|
||||
@after-enter="afterEndTransition"
|
||||
>
|
||||
<div v-if="isCompact" ref="compactEl" class="commit-form-compact">
|
||||
<div
|
||||
v-gl-tooltip="{ title: commitButtonTooltip }"
|
||||
data-testid="begin-commit-button-tooltip"
|
||||
>
|
||||
<gl-button
|
||||
:disabled="commitButtonDisabled"
|
||||
category="primary"
|
||||
variant="confirm"
|
||||
block
|
||||
data-testid="begin-commit-button"
|
||||
@click="beginCommit"
|
||||
>
|
||||
{{ __('Create commit…') }}
|
||||
</gl-button>
|
||||
</div>
|
||||
<p class="gl-text-center gl-font-bold">{{ overviewText }}</p>
|
||||
</div>
|
||||
<form v-else ref="formEl" @submit.prevent.stop="commit">
|
||||
<transition name="fade"> <success-message v-show="lastCommitMsg" /> </transition>
|
||||
<commit-message-field
|
||||
:text="commitMessage"
|
||||
:placeholder="preBuiltCommitMessage"
|
||||
@input="updateCommitMessage"
|
||||
@submit="commit"
|
||||
/>
|
||||
<div class="clearfix gl-mt-5">
|
||||
<actions />
|
||||
<div
|
||||
v-gl-tooltip="{ title: commitButtonTooltip }"
|
||||
class="float-left"
|
||||
data-testid="commit-button-tooltip"
|
||||
>
|
||||
<gl-button
|
||||
:disabled="commitButtonDisabled"
|
||||
:loading="submitCommitLoading"
|
||||
data-testid="commit-button"
|
||||
category="primary"
|
||||
variant="confirm"
|
||||
type="submit"
|
||||
>
|
||||
{{ __('Commit') }}
|
||||
</gl-button>
|
||||
</div>
|
||||
<gl-button
|
||||
v-if="!discardDraftButtonDisabled"
|
||||
class="gl-float-right"
|
||||
data-testid="discard-draft"
|
||||
@click="discardDraft"
|
||||
>
|
||||
{{ __('Discard draft') }}
|
||||
</gl-button>
|
||||
<gl-button
|
||||
v-else
|
||||
type="button"
|
||||
class="gl-float-right"
|
||||
category="secondary"
|
||||
variant="default"
|
||||
@click="toggleIsCompact"
|
||||
>
|
||||
{{ __('Collapse') }}
|
||||
</gl-button>
|
||||
</div>
|
||||
<gl-modal
|
||||
ref="commitErrorModal"
|
||||
modal-id="ide-commit-error-modal"
|
||||
:title="lastCommitError.title"
|
||||
:action-primary="commitErrorPrimaryAction.button"
|
||||
:action-cancel="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
|
||||
text: __('Cancel'),
|
||||
} /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
|
||||
@ok="commitErrorPrimaryAction.callback"
|
||||
>
|
||||
<div v-safe-html="lastCommitError.messageHTML"></div>
|
||||
</gl-modal>
|
||||
</form>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
@ -1,129 +0,0 @@
|
||||
<!-- eslint-disable vue/multi-word-component-names -->
|
||||
<script>
|
||||
import { GlButton, GlModal, GlTooltipDirective } from '@gitlab/ui';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapActions } from 'vuex';
|
||||
import { __ } from '~/locale';
|
||||
import ListItem from './list_item.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlButton,
|
||||
ListItem,
|
||||
GlModal,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
props: {
|
||||
fileList: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
stagedList: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
activeFileKey: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
keyPrefix: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
emptyStateText: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: __('No changes'),
|
||||
},
|
||||
},
|
||||
modal: {
|
||||
actionPrimary: {
|
||||
text: __('Discard all changes'),
|
||||
attributes: {
|
||||
variant: 'danger',
|
||||
},
|
||||
},
|
||||
actionCancel: {
|
||||
text: __('Cancel'),
|
||||
attributes: {
|
||||
variant: 'default',
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
filesLength() {
|
||||
return this.fileList.length;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['unstageAllChanges', 'discardAllChanges']),
|
||||
openDiscardModal() {
|
||||
this.$refs.discardAllModal.show();
|
||||
},
|
||||
unstageAndDiscardAllChanges() {
|
||||
this.unstageAllChanges();
|
||||
this.discardAllChanges();
|
||||
},
|
||||
},
|
||||
discardModalText: __(
|
||||
"You will lose all uncommitted changes you've made in this project. This action cannot be undone.",
|
||||
),
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ide-commit-list-container">
|
||||
<header class="multi-file-commit-panel-header gl-mb-0 gl-flex">
|
||||
<div class="flex-fill gl-flex gl-items-center">
|
||||
<strong> {{ __('Changes') }} </strong>
|
||||
<div class="gl-ml-auto gl-flex">
|
||||
<gl-button
|
||||
v-if="!stagedList"
|
||||
v-gl-tooltip
|
||||
:title="__('Discard all changes')"
|
||||
:aria-label="__('Discard all changes')"
|
||||
:disabled="!filesLength"
|
||||
:class="{
|
||||
'disabled-content': !filesLength,
|
||||
}"
|
||||
class="!gl-shadow-none"
|
||||
category="tertiary"
|
||||
icon="remove"
|
||||
data-placement="bottom"
|
||||
data-container="body"
|
||||
data-boundary="viewport"
|
||||
@click="openDiscardModal"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<ul v-if="filesLength" class="multi-file-commit-list list-unstyled gl-mb-0">
|
||||
<li v-for="file in fileList" :key="file.key">
|
||||
<list-item
|
||||
:file="file"
|
||||
:key-prefix="keyPrefix"
|
||||
:staged-list="stagedList"
|
||||
:active-file-key="activeFileKey"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
<p v-else class="multi-file-commit-list form-text gl-text-center gl-text-subtle">
|
||||
{{ emptyStateText }}
|
||||
</p>
|
||||
<gl-modal
|
||||
v-if="!stagedList"
|
||||
ref="discardAllModal"
|
||||
modal-id="discard-all-changes"
|
||||
:title="__('Discard all changes?')"
|
||||
:action-primary="$options.modal.actionPrimary"
|
||||
:action-cancel="$options.modal.actionCancel"
|
||||
@primary="unstageAndDiscardAllChanges"
|
||||
>
|
||||
{{ $options.discardModalText }}
|
||||
</gl-modal>
|
||||
</div>
|
||||
</template>
|
@ -1,103 +0,0 @@
|
||||
<script>
|
||||
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapActions } from 'vuex';
|
||||
import FileIcon from '~/vue_shared/components/file_icon.vue';
|
||||
import getCommitIconMap from '../../commit_icon';
|
||||
import { viewerTypes } from '../../constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlIcon,
|
||||
FileIcon,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
props: {
|
||||
file: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
keyPrefix: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
stagedList: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
activeFileKey: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
iconName() {
|
||||
// name: '-solid' is a false positive: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26#possible-false-positives
|
||||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||
const suffix = this.stagedList ? '-solid' : '';
|
||||
|
||||
return `${getCommitIconMap(this.file).icon}${suffix}`;
|
||||
},
|
||||
iconClass() {
|
||||
return `${getCommitIconMap(this.file).class} ml-auto mr-auto`;
|
||||
},
|
||||
fullKey() {
|
||||
return `${this.keyPrefix}-${this.file.key}`;
|
||||
},
|
||||
isActive() {
|
||||
return this.activeFileKey === this.fullKey;
|
||||
},
|
||||
tooltipTitle() {
|
||||
return this.file.path === this.file.name ? '' : this.file.path;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['discardFileChanges', 'updateViewer', 'openPendingTab']),
|
||||
openFileInEditor() {
|
||||
if (this.file.type === 'tree') return null;
|
||||
|
||||
return this.openPendingTab({
|
||||
file: this.file,
|
||||
keyPrefix: this.keyPrefix,
|
||||
}).then((changeViewer) => {
|
||||
if (changeViewer) {
|
||||
this.updateViewer(viewerTypes.diff);
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="multi-file-commit-list-item position-relative">
|
||||
<div
|
||||
v-gl-tooltip
|
||||
:title="tooltipTitle"
|
||||
:class="{
|
||||
'is-active': isActive,
|
||||
}"
|
||||
class="multi-file-commit-list-path border-0 ml-0 mr-0 gl-w-full"
|
||||
role="button"
|
||||
@click="openFileInEditor"
|
||||
>
|
||||
<span class="multi-file-commit-list-file-path !gl-flex gl-items-center">
|
||||
<file-icon :file-name="file.name" class="gl-mr-3" />
|
||||
<template v-if="file.prevName && file.prevName !== file.name">
|
||||
{{ file.prevName }} →
|
||||
</template>
|
||||
{{ file.name }}
|
||||
</span>
|
||||
<div class="ml-auto gl-flex gl-items-center">
|
||||
<div class="ide-commit-list-changed-icon gl-flex gl-items-center">
|
||||
<gl-icon :name="iconName" :size="16" :class="iconClass" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
@ -1,129 +0,0 @@
|
||||
<script>
|
||||
import { GlPopover } from '@gitlab/ui';
|
||||
import { __, sprintf } from '~/locale';
|
||||
import HelpIcon from '~/vue_shared/components/help_icon/help_icon.vue';
|
||||
import { MAX_TITLE_LENGTH, MAX_BODY_LENGTH } from '../../constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlPopover,
|
||||
HelpIcon,
|
||||
},
|
||||
props: {
|
||||
text: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
scrollTop: 0,
|
||||
isFocused: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
allLines() {
|
||||
return this.text.split('\n').map((line, i) => ({
|
||||
text: line.substr(0, this.getLineLength(i)) || ' ',
|
||||
highlightedText: line.substr(this.getLineLength(i)),
|
||||
}));
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleScroll() {
|
||||
if (this.$refs.textarea) {
|
||||
this.$nextTick(() => {
|
||||
this.scrollTop = this.$refs.textarea.scrollTop;
|
||||
});
|
||||
}
|
||||
},
|
||||
getLineLength(i) {
|
||||
return i === 0 ? MAX_TITLE_LENGTH : MAX_BODY_LENGTH;
|
||||
},
|
||||
onInput(e) {
|
||||
this.$emit('input', e.target.value);
|
||||
},
|
||||
onCtrlEnter() {
|
||||
if (!this.isFocused) return;
|
||||
this.$emit('submit');
|
||||
},
|
||||
updateIsFocused(isFocused) {
|
||||
this.isFocused = isFocused;
|
||||
},
|
||||
},
|
||||
popoverOptions: {
|
||||
triggers: 'hover',
|
||||
placement: 'top',
|
||||
content: sprintf(
|
||||
__(`
|
||||
The character highlighter helps you keep the subject line to %{titleLength} characters
|
||||
and wrap the body at %{bodyLength} so they are readable in git.
|
||||
`),
|
||||
{ titleLength: MAX_TITLE_LENGTH, bodyLength: MAX_BODY_LENGTH },
|
||||
),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<fieldset class="common-note-form ide-commit-message-field">
|
||||
<div
|
||||
:class="{
|
||||
'is-focused': isFocused,
|
||||
}"
|
||||
class="md-area"
|
||||
>
|
||||
<div v-once class="md-header">
|
||||
<ul class="nav-links">
|
||||
<li>
|
||||
{{ __('Commit Message') }}
|
||||
<div id="ide-commit-message-popover-container">
|
||||
<span id="ide-commit-message-question" class="form-text gl-ml-3">
|
||||
<help-icon />
|
||||
</span>
|
||||
<gl-popover
|
||||
target="ide-commit-message-question"
|
||||
container="ide-commit-message-popover-container"
|
||||
v-bind="$options.popoverOptions"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="ide-commit-message-textarea-container">
|
||||
<div class="ide-commit-message-highlights-container">
|
||||
<div
|
||||
:style="{
|
||||
transform: `translate3d(0, ${-scrollTop}px, 0)`,
|
||||
}"
|
||||
class="note-textarea highlights monospace"
|
||||
>
|
||||
<div v-for="(line, index) in allLines" :key="index">
|
||||
<span v-text="line.text"> </span
|
||||
><mark v-show="line.highlightedText" v-text="line.highlightedText"> </mark>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<textarea
|
||||
ref="textarea"
|
||||
:placeholder="placeholder"
|
||||
:value="text"
|
||||
class="note-textarea ide-commit-message-textarea"
|
||||
dir="auto"
|
||||
name="commit-message"
|
||||
@scroll="handleScroll"
|
||||
@input="onInput"
|
||||
@focus="updateIsFocused(true)"
|
||||
@blur="updateIsFocused(false)"
|
||||
@keydown.ctrl.enter="onCtrlEnter"
|
||||
@keydown.meta.enter="onCtrlEnter"
|
||||
>
|
||||
</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</template>
|
@ -1,53 +0,0 @@
|
||||
<script>
|
||||
import { GlTooltipDirective, GlFormCheckbox } from '@gitlab/ui';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { createNamespacedHelpers } from 'vuex';
|
||||
import { s__ } from '~/locale';
|
||||
|
||||
const { mapActions: mapCommitActions, mapGetters: mapCommitGetters } =
|
||||
createNamespacedHelpers('commit');
|
||||
|
||||
export default {
|
||||
components: { GlFormCheckbox },
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
i18n: {
|
||||
newMrText: s__('IDE|Start a new merge request'),
|
||||
tooltipText: s__(
|
||||
'IDE|This option is disabled because you are not allowed to create merge requests in this project.',
|
||||
),
|
||||
},
|
||||
computed: {
|
||||
...mapCommitGetters(['shouldHideNewMrOption', 'shouldDisableNewMrOption', 'shouldCreateMR']),
|
||||
tooltipText() {
|
||||
return this.shouldDisableNewMrOption ? this.$options.i18n.tooltipText : null;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapCommitActions(['toggleShouldCreateMR']),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<fieldset
|
||||
v-if="!shouldHideNewMrOption"
|
||||
v-gl-tooltip="tooltipText"
|
||||
data-testid="new-merge-request-fieldset"
|
||||
class="js-ide-commit-new-mr"
|
||||
:class="{ 'is-disabled': shouldDisableNewMrOption }"
|
||||
>
|
||||
<hr class="gl-mb-4 gl-mt-3" />
|
||||
|
||||
<gl-form-checkbox
|
||||
:disabled="shouldDisableNewMrOption"
|
||||
:checked="shouldCreateMR"
|
||||
@change="toggleShouldCreateMR"
|
||||
>
|
||||
<span class="ide-option-label">
|
||||
{{ $options.i18n.newMrText }}
|
||||
</span>
|
||||
</gl-form-checkbox>
|
||||
</fieldset>
|
||||
</template>
|
@ -1,104 +0,0 @@
|
||||
<script>
|
||||
import {
|
||||
GlTooltipDirective,
|
||||
GlFormRadio,
|
||||
GlFormRadioGroup,
|
||||
GlFormGroup,
|
||||
GlFormInput,
|
||||
} from '@gitlab/ui';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapActions, mapState, mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlFormRadio,
|
||||
GlFormRadioGroup,
|
||||
GlFormGroup,
|
||||
GlFormInput,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
checked: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
showInput: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState('commit', ['commitAction', 'newBranchName']),
|
||||
...mapGetters('commit', ['placeholderBranchName']),
|
||||
tooltipTitle() {
|
||||
return this.disabled ? this.title : '';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions('commit', ['updateCommitAction', 'updateBranchName']),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<fieldset class="gl-mb-2">
|
||||
<gl-form-radio-group
|
||||
v-gl-tooltip="tooltipTitle"
|
||||
:checked="commitAction"
|
||||
:class="{
|
||||
'is-disabled': disabled,
|
||||
}"
|
||||
>
|
||||
<gl-form-radio
|
||||
:value="value"
|
||||
:disabled="disabled"
|
||||
name="commit-action"
|
||||
@change="updateCommitAction(value)"
|
||||
>
|
||||
<span v-if="label" class="ide-option-label">
|
||||
{{ label }}
|
||||
</span>
|
||||
<slot v-else></slot>
|
||||
</gl-form-radio>
|
||||
</gl-form-radio-group>
|
||||
|
||||
<gl-form-group
|
||||
v-if="commitAction === value && showInput"
|
||||
:label="placeholderBranchName"
|
||||
:label-sr-only="true"
|
||||
class="gl-mb-0 gl-ml-6"
|
||||
>
|
||||
<gl-form-input
|
||||
:placeholder="placeholderBranchName"
|
||||
:value="newBranchName"
|
||||
:disabled="disabled"
|
||||
data-testid="ide-new-branch-name"
|
||||
class="gl-font-monospace"
|
||||
@input="updateBranchName($event)"
|
||||
/>
|
||||
</gl-form-group>
|
||||
</fieldset>
|
||||
</template>
|
@ -1,28 +0,0 @@
|
||||
<script>
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapState } from 'vuex';
|
||||
import SafeHtml from '~/vue_shared/directives/safe_html';
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
SafeHtml,
|
||||
},
|
||||
computed: {
|
||||
...mapState(['lastCommitMsg', 'committedStateSvgPath']),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="multi-file-commit-panel-success-message" aria-live="assertive">
|
||||
<div class="svg-content svg-80">
|
||||
<img :src="committedStateSvgPath" :alt="s__('IDE|Successful commit')" />
|
||||
</div>
|
||||
<div class="gl-ml-3 gl-mr-3">
|
||||
<div class="text-content text-center">
|
||||
<h4>{{ __('All changes are committed') }}</h4>
|
||||
<p v-safe-html="lastCommitMsg"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
@ -1,61 +0,0 @@
|
||||
<script>
|
||||
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
|
||||
import { __, sprintf } from '~/locale';
|
||||
import { viewerTypes } from '../constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
},
|
||||
props: {
|
||||
viewer: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
mergeRequestId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
modeDropdownItems() {
|
||||
return [
|
||||
{
|
||||
viewerType: this.$options.viewerTypes.mr,
|
||||
title: sprintf(__('Reviewing (merge request !%{mergeRequestId})'), {
|
||||
mergeRequestId: this.mergeRequestId,
|
||||
}),
|
||||
content: __('Compare changes with the merge request target branch'),
|
||||
},
|
||||
{
|
||||
viewerType: this.$options.viewerTypes.diff,
|
||||
title: __('Reviewing'),
|
||||
content: __('Compare changes with the last commit'),
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
changeMode(mode) {
|
||||
this.$emit('click', mode);
|
||||
},
|
||||
},
|
||||
viewerTypes,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-dropdown :text="__('Edit')" size="small">
|
||||
<gl-dropdown-item
|
||||
v-for="mode in modeDropdownItems"
|
||||
:key="mode.viewerType"
|
||||
is-check-item
|
||||
:is-checked="viewer === mode.viewerType"
|
||||
@click="changeMode(mode.viewerType)"
|
||||
>
|
||||
<strong class="dropdown-menu-inner-title"> {{ mode.title }} </strong>
|
||||
<span class="dropdown-menu-inner-content"> {{ mode.content }} </span>
|
||||
</gl-dropdown-item>
|
||||
</gl-dropdown>
|
||||
</template>
|
@ -1,65 +0,0 @@
|
||||
<script>
|
||||
import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapActions } from 'vuex';
|
||||
import SafeHtml from '~/vue_shared/directives/safe_html';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlAlert,
|
||||
GlLoadingIcon,
|
||||
},
|
||||
directives: {
|
||||
SafeHtml,
|
||||
},
|
||||
props: {
|
||||
message: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isLoading: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
canDismiss() {
|
||||
return !this.message.action;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['setErrorMessage']),
|
||||
doAction() {
|
||||
if (this.isLoading) return;
|
||||
|
||||
this.isLoading = true;
|
||||
|
||||
this.message
|
||||
.action(this.message.actionPayload)
|
||||
.then(() => {
|
||||
this.isLoading = false;
|
||||
})
|
||||
.catch(() => {
|
||||
this.isLoading = false;
|
||||
});
|
||||
},
|
||||
dismiss() {
|
||||
this.setErrorMessage(null);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-alert
|
||||
variant="danger"
|
||||
:dismissible="canDismiss"
|
||||
:primary-button-text="message.actionText"
|
||||
@dismiss="dismiss"
|
||||
@primaryAction="doAction"
|
||||
>
|
||||
<span v-safe-html="message.text"></span>
|
||||
<gl-loading-icon v-show="isLoading" size="sm" inline class="vertical-align-middle ml-1" />
|
||||
</gl-alert>
|
||||
</template>
|
@ -1,96 +0,0 @@
|
||||
<script>
|
||||
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapGetters } from 'vuex';
|
||||
import { n__ } from '~/locale';
|
||||
import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
|
||||
import MrFileIcon from './mr_file_icon.vue';
|
||||
import NewDropdown from './new_dropdown/index.vue';
|
||||
|
||||
export default {
|
||||
name: 'FileRowExtra',
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
components: {
|
||||
GlIcon,
|
||||
NewDropdown,
|
||||
ChangedFileIcon,
|
||||
MrFileIcon,
|
||||
},
|
||||
props: {
|
||||
file: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
dropdownOpen: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'getChangesInFolder',
|
||||
'getUnstagedFilesCountForPath',
|
||||
'getStagedFilesCountForPath',
|
||||
]),
|
||||
isTree() {
|
||||
return this.file.type === 'tree';
|
||||
},
|
||||
folderUnstagedCount() {
|
||||
return this.getUnstagedFilesCountForPath(this.file.path);
|
||||
},
|
||||
folderStagedCount() {
|
||||
return this.getStagedFilesCountForPath(this.file.path);
|
||||
},
|
||||
changesCount() {
|
||||
return this.getChangesInFolder(this.file.path);
|
||||
},
|
||||
folderChangesTooltip() {
|
||||
if (this.changesCount === 0) return undefined;
|
||||
|
||||
return n__('%d changed file', '%d changed files', this.changesCount);
|
||||
},
|
||||
showTreeChangesCount() {
|
||||
return this.isTree && this.changesCount > 0 && !this.file.opened;
|
||||
},
|
||||
isModified() {
|
||||
return this.file.changed || this.file.tempFile || this.file.staged || this.file.prevPath;
|
||||
},
|
||||
showChangedFileIcon() {
|
||||
return !this.isTree && this.isModified;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ide-file-icon-holder gl-float-right">
|
||||
<mr-file-icon v-if="file.mrChange" />
|
||||
<span v-if="showTreeChangesCount" class="ide-tree-changes">
|
||||
{{ changesCount }}
|
||||
<gl-icon
|
||||
v-gl-tooltip.left.viewport
|
||||
:title="folderChangesTooltip"
|
||||
:size="12"
|
||||
data-container="body"
|
||||
data-placement="right"
|
||||
name="file-modified"
|
||||
class="ide-file-modified gl-ml-2"
|
||||
/>
|
||||
</span>
|
||||
<changed-file-icon
|
||||
v-else-if="showChangedFileIcon"
|
||||
:file="file"
|
||||
:show-tooltip="true"
|
||||
:show-staged-icon="false"
|
||||
/>
|
||||
<new-dropdown
|
||||
:type="file.type"
|
||||
:path="file.path"
|
||||
:is-open="dropdownOpen"
|
||||
class="gl-ml-3"
|
||||
v-on="$listeners"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
@ -1,129 +0,0 @@
|
||||
<!-- eslint-disable vue/multi-word-component-names -->
|
||||
<script>
|
||||
import { GlButton, GlDropdown, GlDropdownItem, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapActions, mapGetters, mapState } from 'vuex';
|
||||
import { __ } from '~/locale';
|
||||
|
||||
const barLabel = __('File templates');
|
||||
const templateListDropdownLabel = __('Choose a template…');
|
||||
const templateTypesDropdownLabel = __('Choose a type…');
|
||||
const undoButtonText = __('Undo');
|
||||
|
||||
export default {
|
||||
i18n: {
|
||||
barLabel,
|
||||
templateListDropdownLabel,
|
||||
templateTypesDropdownLabel,
|
||||
undoButtonText,
|
||||
},
|
||||
components: {
|
||||
GlButton,
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
GlLoadingIcon,
|
||||
GlSearchBoxByType,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
search: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['activeFile']),
|
||||
...mapGetters('fileTemplates', ['templateTypes']),
|
||||
...mapState('fileTemplates', [
|
||||
'selectedTemplateType',
|
||||
'updateSuccess',
|
||||
'templates',
|
||||
'isLoading',
|
||||
]),
|
||||
filteredTemplateTypes() {
|
||||
return this.templates.filter((t) => {
|
||||
return t.name.toLowerCase().includes(this.search.toLowerCase());
|
||||
});
|
||||
},
|
||||
showTemplatesDropdown() {
|
||||
return Object.keys(this.selectedTemplateType).length > 0;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
activeFile: 'setInitialType',
|
||||
},
|
||||
mounted() {
|
||||
this.setInitialType();
|
||||
},
|
||||
methods: {
|
||||
...mapActions('fileTemplates', [
|
||||
'setSelectedTemplateType',
|
||||
'fetchTemplate',
|
||||
'fetchTemplateTypes',
|
||||
'undoFileTemplate',
|
||||
]),
|
||||
setInitialType() {
|
||||
const initialTemplateType = this.templateTypes.find((t) => t.name === this.activeFile.name);
|
||||
|
||||
if (initialTemplateType) {
|
||||
this.setSelectedTemplateType(initialTemplateType);
|
||||
}
|
||||
},
|
||||
selectTemplateType(templateType) {
|
||||
this.setSelectedTemplateType(templateType);
|
||||
},
|
||||
selectTemplate(template) {
|
||||
this.fetchTemplate(template);
|
||||
},
|
||||
undo() {
|
||||
this.undoFileTemplate();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="ide-file-templates gl-relative gl-z-1 gl-flex gl-items-center"
|
||||
data-testid="file-templates-bar"
|
||||
>
|
||||
<strong class="gl-mr-3"> {{ $options.i18n.barLabel }} </strong>
|
||||
<gl-dropdown
|
||||
class="gl-mr-6"
|
||||
:text="selectedTemplateType.name || $options.i18n.templateTypesDropdownLabel"
|
||||
>
|
||||
<gl-dropdown-item
|
||||
v-for="template in templateTypes"
|
||||
:key="template.key"
|
||||
@click.prevent="selectTemplateType(template)"
|
||||
>
|
||||
{{ template.name }}
|
||||
</gl-dropdown-item>
|
||||
</gl-dropdown>
|
||||
<gl-dropdown
|
||||
v-if="showTemplatesDropdown"
|
||||
class="gl-mr-6"
|
||||
:text="$options.i18n.templateListDropdownLabel"
|
||||
@show="fetchTemplateTypes"
|
||||
>
|
||||
<template #header>
|
||||
<gl-search-box-by-type v-model.trim="search" />
|
||||
</template>
|
||||
<div>
|
||||
<gl-loading-icon v-if="isLoading" />
|
||||
<template v-else>
|
||||
<gl-dropdown-item
|
||||
v-for="template in filteredTemplateTypes"
|
||||
:key="template.key"
|
||||
@click="selectTemplate(template)"
|
||||
>
|
||||
{{ template.name }}
|
||||
</gl-dropdown-item>
|
||||
</template>
|
||||
</div>
|
||||
</gl-dropdown>
|
||||
<transition name="fade">
|
||||
<gl-button v-show="updateSuccess" category="secondary" variant="default" @click="undo">
|
||||
{{ $options.i18n.undoButtonText }}
|
||||
</gl-button>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
@ -1,238 +0,0 @@
|
||||
<!-- eslint-disable vue/multi-word-component-names -->
|
||||
<script>
|
||||
import { GlButton, GlLoadingIcon, GlBroadcastMessage, GlLink, GlSprintf } from '@gitlab/ui';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapActions, mapGetters, mapState } from 'vuex';
|
||||
import { __ } from '~/locale';
|
||||
import {
|
||||
WEBIDE_MARK_APP_START,
|
||||
WEBIDE_MARK_FILE_FINISH,
|
||||
WEBIDE_MARK_FILE_CLICKED,
|
||||
WEBIDE_MEASURE_FILE_AFTER_INTERACTION,
|
||||
WEBIDE_MEASURE_BEFORE_VUE,
|
||||
} from '~/performance/constants';
|
||||
import { performanceMarkAndMeasure } from '~/performance/utils';
|
||||
import { helpPagePath } from '~/helpers/help_page_helper';
|
||||
import { modalTypes } from '../constants';
|
||||
import eventHub from '../eventhub';
|
||||
import { measurePerformance } from '../utils';
|
||||
import CannotPushCodeAlert from './cannot_push_code_alert.vue';
|
||||
import IdeSidebar from './ide_side_bar.vue';
|
||||
import RepoEditor from './repo_editor.vue';
|
||||
|
||||
eventHub.$on(WEBIDE_MEASURE_FILE_AFTER_INTERACTION, () =>
|
||||
measurePerformance(
|
||||
WEBIDE_MARK_FILE_FINISH,
|
||||
WEBIDE_MEASURE_FILE_AFTER_INTERACTION,
|
||||
WEBIDE_MARK_FILE_CLICKED,
|
||||
),
|
||||
);
|
||||
|
||||
const removalAnnouncementHelpPagePath = helpPagePath(
|
||||
'update/deprecations.md#legacy-web-ide-is-deprecated',
|
||||
);
|
||||
|
||||
export default {
|
||||
components: {
|
||||
IdeSidebar,
|
||||
RepoEditor,
|
||||
GlButton,
|
||||
GlLoadingIcon,
|
||||
ErrorMessage: () => import(/* webpackChunkName: 'ide_runtime' */ './error_message.vue'),
|
||||
CommitEditorHeader: () =>
|
||||
import(/* webpackChunkName: 'ide_runtime' */ './commit_sidebar/editor_header.vue'),
|
||||
RepoTabs: () => import(/* webpackChunkName: 'ide_runtime' */ './repo_tabs.vue'),
|
||||
IdeStatusBar: () => import(/* webpackChunkName: 'ide_runtime' */ './ide_status_bar.vue'),
|
||||
FindFile: () =>
|
||||
import(/* webpackChunkName: 'ide_runtime' */ '~/vue_shared/components/file_finder/index.vue'),
|
||||
RightPane: () => import(/* webpackChunkName: 'ide_runtime' */ './panes/right.vue'),
|
||||
NewModal: () => import(/* webpackChunkName: 'ide_runtime' */ './new_dropdown/modal.vue'),
|
||||
CannotPushCodeAlert,
|
||||
GlBroadcastMessage,
|
||||
GlLink,
|
||||
GlSprintf,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loadDeferred: false,
|
||||
skipBeforeUnload: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'openFiles',
|
||||
'viewer',
|
||||
'fileFindVisible',
|
||||
'emptyStateSvgPath',
|
||||
'currentProjectId',
|
||||
'errorMessage',
|
||||
'loading',
|
||||
]),
|
||||
...mapGetters([
|
||||
'canPushCodeStatus',
|
||||
'activeFile',
|
||||
'someUncommittedChanges',
|
||||
'isCommitModeActive',
|
||||
'allBlobs',
|
||||
'emptyRepo',
|
||||
'currentTree',
|
||||
'hasCurrentProject',
|
||||
'editorTheme',
|
||||
'getUrlForPath',
|
||||
]),
|
||||
themeName() {
|
||||
return window.gon?.user_color_scheme;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
window.onbeforeunload = (e) => this.onBeforeUnload(e);
|
||||
|
||||
eventHub.$on('skip-beforeunload', this.handleSkipBeforeUnload);
|
||||
|
||||
if (this.themeName)
|
||||
document.querySelector('.navbar-gitlab')?.classList.add(`theme-${this.themeName}`);
|
||||
},
|
||||
destroyed() {
|
||||
eventHub.$off('skip-beforeunload', this.handleSkipBeforeUnload);
|
||||
},
|
||||
beforeCreate() {
|
||||
performanceMarkAndMeasure({
|
||||
mark: WEBIDE_MARK_APP_START,
|
||||
measures: [
|
||||
{
|
||||
name: WEBIDE_MEASURE_BEFORE_VUE,
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['toggleFileFinder']),
|
||||
onBeforeUnload(e = {}) {
|
||||
if (this.skipBeforeUnload) {
|
||||
this.skipBeforeUnload = false;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const returnValue = __('Are you sure you want to lose unsaved changes?');
|
||||
|
||||
if (!this.someUncommittedChanges) return undefined;
|
||||
|
||||
Object.assign(e, {
|
||||
returnValue,
|
||||
});
|
||||
return returnValue;
|
||||
},
|
||||
handleSkipBeforeUnload() {
|
||||
this.skipBeforeUnload = true;
|
||||
},
|
||||
openFile(file) {
|
||||
this.$router.push(this.getUrlForPath(file.path));
|
||||
},
|
||||
createNewFile() {
|
||||
this.$refs.newModal.open(modalTypes.blob);
|
||||
},
|
||||
loadDeferredComponents() {
|
||||
this.loadDeferred = true;
|
||||
},
|
||||
},
|
||||
removalAnnouncementHelpPagePath,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<article
|
||||
class="ide position-relative flex-column align-items-stretch gl-flex"
|
||||
:class="{ [`theme-${themeName}`]: themeName }"
|
||||
>
|
||||
<gl-broadcast-message icon-name="warning" theme="red" :dismissible="false">
|
||||
{{ __('The legacy Vue-based GitLab Web IDE will be removed in GitLab 18.0.') }}
|
||||
<gl-sprintf
|
||||
:message="
|
||||
__('To prepare for this removal, see %{linkStart}deprecations and removals%{linkEnd}.')
|
||||
"
|
||||
>
|
||||
<template #link="{ content }">
|
||||
<gl-link class="!gl-text-inherit" :href="$options.removalAnnouncementHelpPagePath">{{
|
||||
content
|
||||
}}</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</gl-broadcast-message>
|
||||
<cannot-push-code-alert
|
||||
v-if="!canPushCodeStatus.isAllowed"
|
||||
:message="canPushCodeStatus.message"
|
||||
:action="canPushCodeStatus.action"
|
||||
/>
|
||||
<error-message v-if="errorMessage" :message="errorMessage" />
|
||||
<div class="ide-view gl-flex gl-grow">
|
||||
<template v-if="loadDeferred">
|
||||
<find-file
|
||||
:files="allBlobs"
|
||||
:visible="fileFindVisible"
|
||||
:loading="loading"
|
||||
@toggle="toggleFileFinder"
|
||||
@click="openFile"
|
||||
/>
|
||||
</template>
|
||||
<ide-sidebar @tree-ready="loadDeferredComponents" />
|
||||
<div class="multi-file-edit-pane">
|
||||
<template v-if="activeFile">
|
||||
<template v-if="loadDeferred">
|
||||
<commit-editor-header v-if="isCommitModeActive" :active-file="activeFile" />
|
||||
<repo-tabs v-else :active-file="activeFile" :files="openFiles" :viewer="viewer" />
|
||||
</template>
|
||||
<repo-editor :file="activeFile" class="multi-file-edit-pane-content" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="ide-empty-state">
|
||||
<div class="row js-empty-state">
|
||||
<div class="col-12">
|
||||
<div class="svg-content svg-150"><img :src="emptyStateSvgPath" /></div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="text-content text-center">
|
||||
<h4>
|
||||
{{ __('Make and review changes in the browser with the Web IDE') }}
|
||||
</h4>
|
||||
<template v-if="emptyRepo">
|
||||
<p>
|
||||
{{
|
||||
__(
|
||||
"Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes.",
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<gl-button
|
||||
variant="confirm"
|
||||
category="primary"
|
||||
:title="__('New file')"
|
||||
:aria-label="__('New file')"
|
||||
@click="createNewFile()"
|
||||
>
|
||||
{{ __('New file') }}
|
||||
</gl-button>
|
||||
</template>
|
||||
<gl-loading-icon v-else-if="!currentTree || currentTree.loading" size="lg" />
|
||||
<p v-else>
|
||||
{{
|
||||
__(
|
||||
"Select a file from the left sidebar to begin editing. Afterwards, you'll be able to commit your changes.",
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<template v-if="loadDeferred">
|
||||
<right-pane v-if="currentProjectId" />
|
||||
</template>
|
||||
</div>
|
||||
<template v-if="loadDeferred">
|
||||
<ide-status-bar />
|
||||
<new-modal ref="newModal" />
|
||||
</template>
|
||||
</article>
|
||||
</template>
|
@ -1,49 +0,0 @@
|
||||
<script>
|
||||
/**
|
||||
* This component is an iterative step towards refactoring and simplifying `vue_shared/components/file_row.vue`
|
||||
* https://gitlab.com/gitlab-org/gitlab/-/merge_requests/23720
|
||||
*/
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapGetters } from 'vuex';
|
||||
import FileRow from '~/vue_shared/components/file_row.vue';
|
||||
import FileRowExtra from './file_row_extra.vue';
|
||||
|
||||
export default {
|
||||
name: 'IdeFileRow',
|
||||
components: {
|
||||
FileRow,
|
||||
FileRowExtra,
|
||||
},
|
||||
props: {
|
||||
file: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dropdownOpen: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['getUrlForPath']),
|
||||
},
|
||||
methods: {
|
||||
toggleDropdown(val) {
|
||||
this.dropdownOpen = val;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<file-row
|
||||
:file="file"
|
||||
:file-url="getUrlForPath(file.path)"
|
||||
v-bind="$attrs"
|
||||
@mouseleave="toggleDropdown(false)"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<file-row-extra :file="file" :dropdown-open="dropdownOpen" @toggle="toggleDropdown($event)" />
|
||||
</file-row>
|
||||
</template>
|
@ -1,34 +0,0 @@
|
||||
<script>
|
||||
import ProjectAvatar from '~/vue_shared/components/project_avatar.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ProjectAvatar,
|
||||
},
|
||||
props: {
|
||||
project: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="context-header ide-context-header">
|
||||
<a :href="project.web_url" :title="s__('IDE|Go to project')" data-testid="go-to-project-link">
|
||||
<project-avatar
|
||||
:project-id="project.id"
|
||||
:project-name="project.name"
|
||||
:project-avatar-url="project.avatar_url"
|
||||
:size="48"
|
||||
/>
|
||||
<span class="ide-sidebar-project-title">
|
||||
<span class="sidebar-context-title"> {{ project.name }} </span>
|
||||
<span class="sidebar-context-title gl-font-normal gl-text-subtle">
|
||||
{{ project.path_with_namespace }}
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
@ -1,78 +0,0 @@
|
||||
<script>
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapGetters, mapState, mapActions } from 'vuex';
|
||||
import { viewerTypes } from '../constants';
|
||||
import EditorModeDropdown from './editor_mode_dropdown.vue';
|
||||
import IdeTreeList from './ide_tree_list.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
IdeTreeList,
|
||||
EditorModeDropdown,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['currentMergeRequest', 'activeFile', 'getUrlForPath']),
|
||||
...mapState(['viewer', 'currentMergeRequestId']),
|
||||
showLatestChangesText() {
|
||||
return !this.currentMergeRequestId || this.viewer === viewerTypes.diff;
|
||||
},
|
||||
showMergeRequestText() {
|
||||
return this.currentMergeRequestId && this.viewer === viewerTypes.mr;
|
||||
},
|
||||
mergeRequestId() {
|
||||
return `!${this.currentMergeRequest.iid}`;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.initialize();
|
||||
},
|
||||
activated() {
|
||||
this.initialize();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['updateViewer', 'resetOpenFiles']),
|
||||
initialize() {
|
||||
if (this.activeFile && this.activeFile.pending && !this.activeFile.deleted) {
|
||||
this.$router.push(this.getUrlForPath(this.activeFile.path), () => {
|
||||
this.updateViewer(viewerTypes.edit);
|
||||
});
|
||||
} else if (this.activeFile && this.activeFile.deleted) {
|
||||
this.resetOpenFiles();
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.updateViewer(this.currentMergeRequestId ? viewerTypes.mr : viewerTypes.diff);
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ide-tree-list header-class="ide-review-header">
|
||||
<template #header>
|
||||
<div class="ide-review-button-holder">
|
||||
{{ __('Review') }}
|
||||
<editor-mode-dropdown
|
||||
v-if="currentMergeRequest"
|
||||
:viewer="viewer"
|
||||
:merge-request-id="currentMergeRequest.iid"
|
||||
@click="updateViewer"
|
||||
/>
|
||||
</div>
|
||||
<div class="ide-review-sub-header gl-mt-2">
|
||||
<template v-if="showLatestChangesText">
|
||||
{{ __('Latest changes') }}
|
||||
</template>
|
||||
<template v-else-if="showMergeRequestText">
|
||||
{{ __('Merge request') }} (<a
|
||||
v-if="currentMergeRequest"
|
||||
:href="currentMergeRequest.web_url"
|
||||
v-text="mergeRequestId"
|
||||
></a
|
||||
>)
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</ide-tree-list>
|
||||
</template>
|
@ -1,61 +0,0 @@
|
||||
<script>
|
||||
import { GlSkeletonLoader } from '@gitlab/ui';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapState, mapGetters } from 'vuex';
|
||||
import { SIDEBAR_INIT_WIDTH, leftSidebarViews } from '../constants';
|
||||
import ActivityBar from './activity_bar.vue';
|
||||
import CommitForm from './commit_sidebar/form.vue';
|
||||
import IdeProjectHeader from './ide_project_header.vue';
|
||||
import IdeTree from './ide_tree.vue';
|
||||
import ResizablePanel from './resizable_panel.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlSkeletonLoader,
|
||||
ResizablePanel,
|
||||
ActivityBar,
|
||||
IdeTree,
|
||||
[leftSidebarViews.review.name]: () =>
|
||||
import(/* webpackChunkName: 'ide_runtime' */ './ide_review.vue'),
|
||||
[leftSidebarViews.commit.name]: () =>
|
||||
import(/* webpackChunkName: 'ide_runtime' */ './repo_commit_section.vue'),
|
||||
CommitForm,
|
||||
IdeProjectHeader,
|
||||
},
|
||||
computed: {
|
||||
...mapState(['loading', 'currentActivityView', 'changedFiles', 'stagedFiles', 'lastCommitMsg']),
|
||||
...mapGetters(['currentProject', 'someUncommittedChanges']),
|
||||
},
|
||||
SIDEBAR_INIT_WIDTH,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<resizable-panel
|
||||
:initial-width="$options.SIDEBAR_INIT_WIDTH"
|
||||
side="left"
|
||||
class="multi-file-commit-panel flex-column"
|
||||
>
|
||||
<template v-if="loading">
|
||||
<div class="multi-file-commit-panel-inner" data-testid="ide-side-bar-inner">
|
||||
<div v-for="n in 3" :key="n" class="multi-file-loading-container">
|
||||
<gl-skeleton-loader />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<ide-project-header :project="currentProject" />
|
||||
<div class="ide-context-body flex-fill gl-flex">
|
||||
<activity-bar />
|
||||
<div class="multi-file-commit-panel-inner" data-testid="ide-side-bar-inner">
|
||||
<div class="multi-file-commit-panel-inner-content">
|
||||
<keep-alive>
|
||||
<component :is="currentActivityView" @tree-ready="$emit('tree-ready')" />
|
||||
</keep-alive>
|
||||
</div>
|
||||
<commit-form />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</resizable-panel>
|
||||
</template>
|
@ -1,83 +0,0 @@
|
||||
<script>
|
||||
import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
|
||||
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
|
||||
import { SIDE_RIGHT } from '../constants';
|
||||
import { otherSide } from '../utils';
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
components: {
|
||||
GlIcon,
|
||||
},
|
||||
props: {
|
||||
tabs: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
side: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
currentView: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
isOpen: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
otherSide() {
|
||||
return otherSide(this.side);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
isActiveTab(tab) {
|
||||
return this.isOpen && tab.views.some((view) => view.name === this.currentView);
|
||||
},
|
||||
buttonClasses(tab) {
|
||||
return [
|
||||
{
|
||||
'is-right': this.side === SIDE_RIGHT,
|
||||
active: this.isActiveTab(tab),
|
||||
},
|
||||
...(tab.buttonClasses || []),
|
||||
];
|
||||
},
|
||||
clickTab(e, tab) {
|
||||
e.currentTarget.blur();
|
||||
this.$root.$emit(BV_HIDE_TOOLTIP);
|
||||
|
||||
if (this.isActiveTab(tab)) {
|
||||
this.$emit('close');
|
||||
} else {
|
||||
this.$emit('open', tab.views[0]);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<nav class="ide-activity-bar">
|
||||
<ul class="list-unstyled">
|
||||
<li v-for="tab of tabs" :key="tab.title">
|
||||
<button
|
||||
v-gl-tooltip="{ container: 'body', placement: otherSide }"
|
||||
:title="tab.title"
|
||||
:aria-label="tab.title"
|
||||
class="ide-sidebar-link"
|
||||
:class="buttonClasses(tab)"
|
||||
type="button"
|
||||
@click="clickTab($event, tab)"
|
||||
>
|
||||
<gl-icon :size="16" :name="tab.icon" />
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</template>
|
@ -1,137 +0,0 @@
|
||||
<script>
|
||||
/* eslint-disable @gitlab/vue-require-i18n-strings */
|
||||
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapActions, mapState, mapGetters } from 'vuex';
|
||||
import timeAgoMixin from '~/vue_shared/mixins/timeago';
|
||||
import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
|
||||
import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
|
||||
import { rightSidebarViews } from '../constants';
|
||||
import IdeStatusList from './ide_status_list.vue';
|
||||
import IdeStatusMr from './ide_status_mr.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlIcon,
|
||||
UserAvatarImage,
|
||||
CiIcon,
|
||||
IdeStatusList,
|
||||
IdeStatusMr,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
mixins: [timeAgoMixin],
|
||||
data() {
|
||||
return {
|
||||
lastCommitFormattedAge: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(['currentBranchId', 'currentProjectId']),
|
||||
...mapGetters(['currentProject', 'lastCommit', 'currentMergeRequest']),
|
||||
...mapState('pipelines', ['latestPipeline']),
|
||||
},
|
||||
watch: {
|
||||
lastCommit: {
|
||||
handler() {
|
||||
this.initPipelinePolling();
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.startTimer();
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.intervalId) {
|
||||
clearInterval(this.intervalId);
|
||||
}
|
||||
|
||||
this.stopPipelinePolling();
|
||||
},
|
||||
methods: {
|
||||
...mapActions('rightPane', {
|
||||
openRightPane: 'open',
|
||||
}),
|
||||
...mapActions('pipelines', ['fetchLatestPipeline', 'stopPipelinePolling']),
|
||||
startTimer() {
|
||||
this.intervalId = setInterval(() => {
|
||||
this.commitAgeUpdate();
|
||||
}, 1000);
|
||||
},
|
||||
initPipelinePolling() {
|
||||
if (this.lastCommit) {
|
||||
this.fetchLatestPipeline();
|
||||
}
|
||||
},
|
||||
commitAgeUpdate() {
|
||||
if (this.lastCommit) {
|
||||
this.lastCommitFormattedAge = this.timeFormatted(this.lastCommit.committed_date);
|
||||
}
|
||||
},
|
||||
getCommitPath(shortSha) {
|
||||
return `${this.currentProject.web_url}/commit/${shortSha}`;
|
||||
},
|
||||
},
|
||||
rightSidebarViews,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer class="ide-status-bar">
|
||||
<div v-if="lastCommit" class="ide-status-branch">
|
||||
<span v-if="latestPipeline && latestPipeline.details" class="ide-status-pipeline">
|
||||
<button
|
||||
type="button"
|
||||
class="p-0 border-0 bg-transparent"
|
||||
@click="openRightPane($options.rightSidebarViews.pipelines)"
|
||||
>
|
||||
<ci-icon
|
||||
:status="latestPipeline.details.status"
|
||||
:title="latestPipeline.details.status.text"
|
||||
/>
|
||||
</button>
|
||||
Pipeline
|
||||
<a :href="latestPipeline.details.status.details_path" class="monospace"
|
||||
>#{{ latestPipeline.id }}</a
|
||||
>
|
||||
{{ latestPipeline.details.status.text }} for
|
||||
</span>
|
||||
|
||||
<gl-icon name="commit" />
|
||||
<a
|
||||
v-gl-tooltip
|
||||
:title="lastCommit.message"
|
||||
:href="getCommitPath(lastCommit.short_id)"
|
||||
class="commit-sha"
|
||||
data-testid="commit-sha-content"
|
||||
>{{ lastCommit.short_id }}</a
|
||||
>
|
||||
by
|
||||
<user-avatar-image
|
||||
css-classes="ide-status-avatar"
|
||||
:size="16"
|
||||
:img-src="latestPipeline && latestPipeline.commit.author_gravatar_url"
|
||||
:img-alt="lastCommit.author_name"
|
||||
:tooltip-text="lastCommit.author_name"
|
||||
/>
|
||||
{{ lastCommit.author_name }}
|
||||
<time
|
||||
v-gl-tooltip
|
||||
:datetime="lastCommit.committed_date"
|
||||
:title="tooltipTitle(lastCommit.committed_date)"
|
||||
data-placement="top"
|
||||
data-container="body"
|
||||
>{{ lastCommitFormattedAge }}</time
|
||||
>
|
||||
</div>
|
||||
<ide-status-mr
|
||||
v-if="currentMergeRequest"
|
||||
class="mx-3"
|
||||
:url="currentMergeRequest.web_url"
|
||||
:text="currentMergeRequest.references.short"
|
||||
/>
|
||||
<ide-status-list class="ml-auto" />
|
||||
</footer>
|
||||
</template>
|
@ -1,45 +0,0 @@
|
||||
<script>
|
||||
import { GlLink, GlTooltipDirective } from '@gitlab/ui';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapGetters } from 'vuex';
|
||||
import { isTextFile, getFileEOL } from '~/ide/utils';
|
||||
import TerminalSyncStatusSafe from './terminal_sync/terminal_sync_status_safe.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlLink,
|
||||
TerminalSyncStatusSafe,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['activeFile']),
|
||||
...mapGetters('editor', ['activeFileEditor']),
|
||||
activeFileEOL() {
|
||||
return getFileEOL(this.activeFile.content);
|
||||
},
|
||||
activeFileIsText() {
|
||||
return isTextFile(this.activeFile);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ide-status-list gl-flex">
|
||||
<template v-if="activeFile">
|
||||
<div>
|
||||
<gl-link v-gl-tooltip.hover :href="activeFile.permalink" :title="__('Open in file view')">
|
||||
{{ activeFile.name }}
|
||||
</gl-link>
|
||||
</div>
|
||||
<div>{{ activeFileEOL }}</div>
|
||||
<div v-if="activeFileIsText">
|
||||
{{ activeFileEditor.editorRow }}:{{ activeFileEditor.editorColumn }}
|
||||
</div>
|
||||
<div>{{ activeFileEditor.fileLanguage }}</div>
|
||||
</template>
|
||||
<terminal-sync-status-safe />
|
||||
</div>
|
||||
</template>
|
@ -1,30 +0,0 @@
|
||||
<script>
|
||||
import { GlIcon, GlLink } from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlIcon,
|
||||
GlLink,
|
||||
},
|
||||
props: {
|
||||
text: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="text-nowrap js-ide-status-mr gl-flex gl-flex-nowrap gl-items-center gl-justify-center"
|
||||
>
|
||||
<gl-icon name="merge-request" />
|
||||
<span class="ml-1 gl-hidden sm:gl-block">{{ s__('WebIDE|Merge request') }}</span>
|
||||
<gl-link class="ml-1" :href="url">{{ text }}</gl-link>
|
||||
</div>
|
||||
</template>
|
@ -1,83 +0,0 @@
|
||||
<script>
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapState, mapGetters, mapActions } from 'vuex';
|
||||
import { modalTypes, viewerTypes } from '../constants';
|
||||
import IdeTreeList from './ide_tree_list.vue';
|
||||
import NewEntryButton from './new_dropdown/button.vue';
|
||||
import NewModal from './new_dropdown/modal.vue';
|
||||
import Upload from './new_dropdown/upload.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Upload,
|
||||
IdeTreeList,
|
||||
NewEntryButton,
|
||||
NewModal,
|
||||
},
|
||||
computed: {
|
||||
...mapState(['currentBranchId']),
|
||||
...mapGetters(['currentProject', 'currentTree', 'activeFile', 'getUrlForPath']),
|
||||
},
|
||||
mounted() {
|
||||
this.initialize();
|
||||
},
|
||||
activated() {
|
||||
this.initialize();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['updateViewer', 'createTempEntry', 'resetOpenFiles']),
|
||||
createNewFile() {
|
||||
this.$refs.newModal.open(modalTypes.blob);
|
||||
},
|
||||
createNewFolder() {
|
||||
this.$refs.newModal.open(modalTypes.tree);
|
||||
},
|
||||
initialize() {
|
||||
this.$nextTick(() => {
|
||||
this.updateViewer(viewerTypes.edit);
|
||||
});
|
||||
|
||||
if (!this.activeFile) return;
|
||||
|
||||
if (this.activeFile.pending && !this.activeFile.deleted) {
|
||||
this.$router.push(this.getUrlForPath(this.activeFile.path), () => {
|
||||
this.updateViewer(viewerTypes.edit);
|
||||
});
|
||||
} else if (this.activeFile.deleted) {
|
||||
this.resetOpenFiles();
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ide-tree-list @tree-ready="$emit('tree-ready')">
|
||||
<template #header>
|
||||
{{ __('Edit') }}
|
||||
<div class="ide-tree-actions gl-ml-auto gl-flex" data-testid="ide-root-actions">
|
||||
<new-entry-button
|
||||
:label="__('New file')"
|
||||
:show-label="false"
|
||||
class="gl-mr-5 gl-flex gl-border-0 gl-p-0"
|
||||
icon="doc-new"
|
||||
@click="createNewFile()"
|
||||
/>
|
||||
<upload
|
||||
:show-label="false"
|
||||
class="gl-mr-5 gl-flex"
|
||||
button-css-classes="gl-border-0 gl-p-0"
|
||||
@create="createTempEntry"
|
||||
/>
|
||||
<new-entry-button
|
||||
:label="__('New directory')"
|
||||
:show-label="false"
|
||||
class="gl-flex gl-border-0 gl-p-0"
|
||||
icon="folder-new"
|
||||
@click="createNewFolder()"
|
||||
/>
|
||||
</div>
|
||||
<new-modal ref="newModal" />
|
||||
</template>
|
||||
</ide-tree-list>
|
||||
</template>
|
@ -1,83 +0,0 @@
|
||||
<script>
|
||||
import { GlSkeletonLoader } from '@gitlab/ui';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapActions, mapGetters, mapState } from 'vuex';
|
||||
import { WEBIDE_MARK_FILE_CLICKED } from '~/performance/constants';
|
||||
import { performanceMarkAndMeasure } from '~/performance/utils';
|
||||
import FileTree from '~/vue_shared/components/file_tree.vue';
|
||||
import IdeFileRow from './ide_file_row.vue';
|
||||
import NavDropdown from './nav_dropdown.vue';
|
||||
|
||||
export default {
|
||||
name: 'IdeTreeList',
|
||||
components: {
|
||||
GlSkeletonLoader,
|
||||
NavDropdown,
|
||||
FileTree,
|
||||
},
|
||||
props: {
|
||||
headerClass: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(['currentBranchId']),
|
||||
...mapGetters(['currentProject', 'currentTree']),
|
||||
showLoading() {
|
||||
return !this.currentTree || this.currentTree.loading;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
showLoading() {
|
||||
this.notifyTreeReady();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.notifyTreeReady();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['toggleTreeOpen']),
|
||||
notifyTreeReady() {
|
||||
if (!this.showLoading) {
|
||||
this.$emit('tree-ready');
|
||||
}
|
||||
},
|
||||
clickedFile() {
|
||||
performanceMarkAndMeasure({ mark: WEBIDE_MARK_FILE_CLICKED });
|
||||
},
|
||||
},
|
||||
IdeFileRow,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ide-file-list">
|
||||
<template v-if="showLoading">
|
||||
<div v-for="n in 3" :key="n" class="multi-file-loading-container">
|
||||
<gl-skeleton-loader />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<header :class="headerClass" class="ide-tree-header">
|
||||
<nav-dropdown />
|
||||
<slot name="header"></slot>
|
||||
</header>
|
||||
<div class="ide-tree-body gl-h-full" data-testid="ide-tree-body">
|
||||
<template v-if="currentTree.tree.length">
|
||||
<file-tree
|
||||
v-for="file in currentTree.tree"
|
||||
:key="file.key"
|
||||
:file="file"
|
||||
:level="0"
|
||||
:file-row-component="$options.IdeFileRow"
|
||||
@toggleTreeOpen="toggleTreeOpen"
|
||||
@clickFile="clickedFile"
|
||||
/>
|
||||
</template>
|
||||
<div v-else class="file-row">{{ __('No files') }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
@ -1,121 +0,0 @@
|
||||
<!-- eslint-disable vue/multi-word-component-names -->
|
||||
<script>
|
||||
import { GlTooltipDirective, GlButton, GlIcon } from '@gitlab/ui';
|
||||
import { throttle } from 'lodash';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapActions, mapState } from 'vuex';
|
||||
import SafeHtml from '~/vue_shared/directives/safe_html';
|
||||
import { __ } from '~/locale';
|
||||
import JobDescription from './detail/description.vue';
|
||||
import ScrollButton from './detail/scroll_button.vue';
|
||||
|
||||
const scrollPositions = {
|
||||
top: 0,
|
||||
bottom: 1,
|
||||
};
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
SafeHtml,
|
||||
},
|
||||
components: {
|
||||
GlButton,
|
||||
GlIcon,
|
||||
ScrollButton,
|
||||
JobDescription,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
scrollPos: scrollPositions.top,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState('pipelines', ['detailJob']),
|
||||
isScrolledToBottom() {
|
||||
return this.scrollPos === scrollPositions.bottom;
|
||||
},
|
||||
isScrolledToTop() {
|
||||
return this.scrollPos === scrollPositions.top;
|
||||
},
|
||||
jobOutput() {
|
||||
return this.detailJob.output || __('No messages were logged');
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.getLogs();
|
||||
},
|
||||
methods: {
|
||||
...mapActions('pipelines', ['fetchJobLogs', 'setDetailJob']),
|
||||
scrollDown() {
|
||||
if (this.$refs.buildJobLog) {
|
||||
this.$refs.buildJobLog.scrollTo(0, this.$refs.buildJobLog.scrollHeight);
|
||||
}
|
||||
},
|
||||
scrollUp() {
|
||||
if (this.$refs.buildJobLog) {
|
||||
this.$refs.buildJobLog.scrollTo(0, 0);
|
||||
}
|
||||
},
|
||||
scrollBuildLog: throttle(function buildLogScrollDebounce() {
|
||||
const { scrollTop } = this.$refs.buildJobLog;
|
||||
const { offsetHeight, scrollHeight } = this.$refs.buildJobLog;
|
||||
|
||||
if (scrollTop + offsetHeight === scrollHeight) {
|
||||
this.scrollPos = scrollPositions.bottom;
|
||||
} else if (scrollTop === 0) {
|
||||
this.scrollPos = scrollPositions.top;
|
||||
} else {
|
||||
this.scrollPos = '';
|
||||
}
|
||||
}),
|
||||
getLogs() {
|
||||
return this.fetchJobLogs().then(() => this.scrollDown());
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ide-pipeline build-page flex-column flex-fill gl-flex">
|
||||
<header class="ide-job-header gl-flex gl-items-center">
|
||||
<gl-button category="secondary" icon="chevron-left" size="small" @click="setDetailJob(null)">
|
||||
{{ __('View jobs') }}
|
||||
</gl-button>
|
||||
</header>
|
||||
<div class="top-bar border-left-0 mr-3 gl-flex">
|
||||
<job-description :job="detailJob" />
|
||||
<div class="controllers ml-auto">
|
||||
<a
|
||||
v-gl-tooltip
|
||||
:title="__('Show complete raw log')"
|
||||
:href="detailJob.rawPath"
|
||||
data-placement="top"
|
||||
data-container="body"
|
||||
class="controllers-buttons"
|
||||
target="_blank"
|
||||
>
|
||||
<gl-icon name="doc-text" />
|
||||
</a>
|
||||
<scroll-button :disabled="isScrolledToTop" direction="up" @click="scrollUp" />
|
||||
<scroll-button :disabled="isScrolledToBottom" direction="down" @click="scrollDown" />
|
||||
</div>
|
||||
</div>
|
||||
<pre ref="buildJobLog" class="build-log mb-0 mr-3 gl-h-full" @scroll="scrollBuildLog">
|
||||
<code
|
||||
v-show="!detailJob.isLoading"
|
||||
v-safe-html="jobOutput"
|
||||
class="bash"
|
||||
>
|
||||
</code>
|
||||
<div
|
||||
v-show="detailJob.isLoading"
|
||||
class="build-loader-animation"
|
||||
>
|
||||
<div class="dot"></div>
|
||||
<div class="dot"></div>
|
||||
<div class="dot"></div>
|
||||
</div>
|
||||
</pre>
|
||||
</div>
|
||||
</template>
|
@ -1,41 +0,0 @@
|
||||
<!-- eslint-disable vue/multi-word-component-names -->
|
||||
<script>
|
||||
import { GlIcon } from '@gitlab/ui';
|
||||
import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlIcon,
|
||||
CiIcon,
|
||||
},
|
||||
props: {
|
||||
job: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
jobId() {
|
||||
return `#${this.job.id}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gl-flex gl-items-center">
|
||||
<ci-icon :status="job.status" class="gl-border gl-z-1 gl-inline-flex gl-items-center" />
|
||||
<span class="gl-ml-3">
|
||||
{{ job.name }}
|
||||
<a
|
||||
v-if="job.path"
|
||||
:href="job.path"
|
||||
target="_blank"
|
||||
class="ide-external-link gl-relative"
|
||||
data-testid="description-detail-link"
|
||||
>
|
||||
{{ jobId }} <gl-icon :size="12" name="external-link" />
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
@ -1,65 +0,0 @@
|
||||
<script>
|
||||
import { GlIcon, GlTooltipDirective, GlButton } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
|
||||
const directions = {
|
||||
up: 'up',
|
||||
down: 'down',
|
||||
};
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
components: {
|
||||
GlButton,
|
||||
GlIcon,
|
||||
},
|
||||
props: {
|
||||
direction: {
|
||||
type: String,
|
||||
required: true,
|
||||
validator(value) {
|
||||
return Object.keys(directions).includes(value);
|
||||
},
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
tooltipTitle() {
|
||||
return this.direction === directions.up ? __('Scroll to top') : __('Scroll to bottom');
|
||||
},
|
||||
iconName() {
|
||||
return `scroll_${this.direction}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
clickedScroll() {
|
||||
this.$emit('click');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-gl-tooltip
|
||||
:title="tooltipTitle"
|
||||
class="controllers-buttons"
|
||||
data-container="body"
|
||||
data-placement="top"
|
||||
>
|
||||
<gl-button
|
||||
:disabled="disabled"
|
||||
class="!gl-m-0 gl-block !gl-min-w-0 gl-rounded-none !gl-border-0 !gl-border-none gl-bg-transparent !gl-p-0 !gl-shadow-none !gl-outline-none"
|
||||
type="button"
|
||||
:aria-label="tooltipTitle"
|
||||
@click="clickedScroll"
|
||||
>
|
||||
<gl-icon :name="iconName" />
|
||||
</gl-button>
|
||||
</div>
|
||||
</template>
|
@ -1,39 +0,0 @@
|
||||
<!-- eslint-disable vue/multi-word-component-names -->
|
||||
<script>
|
||||
import { GlButton } from '@gitlab/ui';
|
||||
import JobDescription from './detail/description.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
JobDescription,
|
||||
GlButton,
|
||||
},
|
||||
props: {
|
||||
job: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
jobId() {
|
||||
return `#${this.job.id}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
clickViewLog() {
|
||||
this.$emit('clickViewLog', this.job);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ide-job-item">
|
||||
<job-description :job="job" class="gl-mr-3" />
|
||||
<div class="ml-auto align-self-center">
|
||||
<gl-button v-if="job.started" category="secondary" size="small" @click="clickViewLog">
|
||||
{{ __('View log') }}
|
||||
</gl-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
@ -1,43 +0,0 @@
|
||||
<!-- eslint-disable vue/multi-word-component-names -->
|
||||
<script>
|
||||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapActions } from 'vuex';
|
||||
import Stage from './stage.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Stage,
|
||||
GlLoadingIcon,
|
||||
},
|
||||
props: {
|
||||
stages: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions('pipelines', ['fetchJobs', 'toggleStageCollapsed', 'setDetailJob']),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<gl-loading-icon v-if="loading && !stages.length" size="lg" class="gl-mt-3" />
|
||||
<template v-else>
|
||||
<stage
|
||||
v-for="stage in stages"
|
||||
:key="stage.id"
|
||||
:stage="stage"
|
||||
@fetch="fetchJobs"
|
||||
@toggleCollapsed="toggleStageCollapsed"
|
||||
@clickViewLog="setDetailJob"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
@ -1,93 +0,0 @@
|
||||
<!-- eslint-disable vue/multi-word-component-names -->
|
||||
<script>
|
||||
import { GlLoadingIcon, GlIcon, GlTooltipDirective, GlBadge } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
import Item from './item.vue';
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
components: {
|
||||
GlIcon,
|
||||
GlBadge,
|
||||
Item,
|
||||
GlLoadingIcon,
|
||||
},
|
||||
props: {
|
||||
stage: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showTooltip: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
collapseIcon() {
|
||||
return this.stage.isCollapsed ? 'chevron-lg-down' : 'chevron-lg-up';
|
||||
},
|
||||
showLoadingIcon() {
|
||||
return this.stage.isLoading && !this.stage.jobs.length;
|
||||
},
|
||||
stageTitle() {
|
||||
const prefix = __('Stage');
|
||||
return `${prefix}: ${this.stage.name}`;
|
||||
},
|
||||
jobsCount() {
|
||||
return this.stage.jobs.length;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const { stageTitle } = this.$refs;
|
||||
|
||||
this.showTooltip = stageTitle.scrollWidth > stageTitle.offsetWidth;
|
||||
|
||||
this.$emit('fetch', this.stage);
|
||||
},
|
||||
methods: {
|
||||
toggleCollapsed() {
|
||||
this.$emit('toggleCollapsed', this.stage.id);
|
||||
},
|
||||
clickViewLog(job) {
|
||||
this.$emit('clickViewLog', job);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ide-stage card gl-mt-3">
|
||||
<div
|
||||
:class="{
|
||||
'border-bottom-0': stage.isCollapsed,
|
||||
}"
|
||||
class="card-header gl-flex gl-cursor-pointer gl-items-center"
|
||||
data-testid="card-header"
|
||||
@click="toggleCollapsed"
|
||||
>
|
||||
<strong
|
||||
ref="stageTitle"
|
||||
v-gl-tooltip="showTooltip"
|
||||
:title="showTooltip ? stage.name : null"
|
||||
data-container="body"
|
||||
class="gl-truncate"
|
||||
data-testid="stage-title"
|
||||
>
|
||||
{{ stageTitle }}
|
||||
</strong>
|
||||
<div v-if="!stage.isLoading || stage.jobs.length" class="gl-ml-2 gl-mr-3">
|
||||
<gl-badge>{{ jobsCount }}</gl-badge>
|
||||
</div>
|
||||
<gl-icon :name="collapseIcon" class="gl-absolute gl-right-5" />
|
||||
</div>
|
||||
<div v-show="!stage.isCollapsed" class="card-body p-0" data-testid="job-list">
|
||||
<gl-loading-icon v-if="showLoadingIcon" size="sm" />
|
||||
<template v-else>
|
||||
<item v-for="job in stage.jobs" :key="job.id" :job="job" @clickViewLog="clickViewLog" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
@ -1,51 +0,0 @@
|
||||
<!-- eslint-disable vue/multi-word-component-names -->
|
||||
<script>
|
||||
import { GlIcon, GlButton } from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlIcon,
|
||||
GlButton,
|
||||
},
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
currentId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
currentProjectId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isActive() {
|
||||
return (
|
||||
this.item.iid === parseInt(this.currentId, 10) &&
|
||||
this.currentProjectId === this.item.projectPathWithNamespace
|
||||
);
|
||||
},
|
||||
pathWithID() {
|
||||
return `${this.item.projectPathWithNamespace}!${this.item.iid}`;
|
||||
},
|
||||
mergeRequestHref() {
|
||||
const path = `/project/${this.item.projectPathWithNamespace}/merge_requests/${this.item.iid}`;
|
||||
|
||||
return this.$router.resolve(path).href;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-button variant="link" :href="mergeRequestHref">
|
||||
<span class="gl-inline-block gl-whitespace-normal">
|
||||
<gl-icon v-if="isActive" :size="16" name="mobile-issue-close" class="gl-mr-3" />
|
||||
<strong> {{ item.title }} </strong>
|
||||
<span class="ide-merge-request-project-path mt-1 gl-block"> {{ pathWithID }} </span>
|
||||
</span>
|
||||
</gl-button>
|
||||
</template>
|
@ -1,126 +0,0 @@
|
||||
<!-- eslint-disable vue/multi-word-component-names -->
|
||||
<script>
|
||||
import { GlLoadingIcon, GlIcon, GlButton } from '@gitlab/ui';
|
||||
import { debounce } from 'lodash';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapActions, mapState } from 'vuex';
|
||||
import { __ } from '~/locale';
|
||||
import TokenedInput from '../shared/tokened_input.vue';
|
||||
import Item from './item.vue';
|
||||
|
||||
const SEARCH_TYPES = [
|
||||
{ type: 'created', label: __('Created by me') },
|
||||
{ type: 'assigned', label: __('Assigned to me') },
|
||||
];
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TokenedInput,
|
||||
Item,
|
||||
GlIcon,
|
||||
GlLoadingIcon,
|
||||
GlButton,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
search: '',
|
||||
currentSearchType: null,
|
||||
hasSearchFocus: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState('mergeRequests', ['mergeRequests', 'isLoading']),
|
||||
...mapState(['currentMergeRequestId', 'currentProjectId']),
|
||||
hasMergeRequests() {
|
||||
return this.mergeRequests.length !== 0;
|
||||
},
|
||||
hasNoSearchResults() {
|
||||
return this.search !== '' && !this.hasMergeRequests;
|
||||
},
|
||||
showSearchTypes() {
|
||||
return this.hasSearchFocus && !this.search && !this.currentSearchType;
|
||||
},
|
||||
type() {
|
||||
return this.currentSearchType ? this.currentSearchType.type : '';
|
||||
},
|
||||
searchTokens() {
|
||||
return this.currentSearchType ? [this.currentSearchType] : [];
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
search() {
|
||||
// When the search is updated, let's turn off this flag to hide the search types
|
||||
this.hasSearchFocus = false;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.loadMergeRequests();
|
||||
},
|
||||
methods: {
|
||||
...mapActions('mergeRequests', ['fetchMergeRequests']),
|
||||
loadMergeRequests() {
|
||||
this.fetchMergeRequests({ type: this.type, search: this.search });
|
||||
},
|
||||
searchMergeRequests: debounce(function debounceSearch() {
|
||||
this.loadMergeRequests();
|
||||
}, 250),
|
||||
onSearchFocus() {
|
||||
this.hasSearchFocus = true;
|
||||
},
|
||||
setSearchType(searchType) {
|
||||
this.currentSearchType = searchType;
|
||||
this.loadMergeRequests();
|
||||
},
|
||||
},
|
||||
searchTypes: SEARCH_TYPES,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<label
|
||||
class="dropdown-input gl-mb-0 gl-block gl-border-b-1 gl-pb-5 gl-pt-3 gl-border-b-solid"
|
||||
@click.stop
|
||||
>
|
||||
<tokened-input
|
||||
v-model="search"
|
||||
:tokens="searchTokens"
|
||||
:placeholder="__('Search merge requests')"
|
||||
@focus="onSearchFocus"
|
||||
@input="searchMergeRequests"
|
||||
@removeToken="setSearchType(null)"
|
||||
/>
|
||||
<gl-icon :size="16" name="search" class="ml-3 input-icon" />
|
||||
</label>
|
||||
<div class="dropdown-content ide-merge-requests-dropdown-content gl-flex">
|
||||
<gl-loading-icon
|
||||
v-if="isLoading"
|
||||
size="lg"
|
||||
class="mt-3 mb-3 align-self-center ml-auto mr-auto"
|
||||
/>
|
||||
<template v-else>
|
||||
<ul class="mb-0 gl-w-full">
|
||||
<template v-if="showSearchTypes">
|
||||
<li v-for="searchType in $options.searchTypes" :key="searchType.type">
|
||||
<gl-button variant="link" icon="search" @click="setSearchType(searchType)">
|
||||
{{ searchType.label }}
|
||||
</gl-button>
|
||||
</li>
|
||||
</template>
|
||||
<template v-else-if="hasMergeRequests">
|
||||
<li v-for="item in mergeRequests" :key="item.id">
|
||||
<item
|
||||
:item="item"
|
||||
:current-id="currentMergeRequestId"
|
||||
:current-project-id="currentProjectId"
|
||||
/>
|
||||
</li>
|
||||
</template>
|
||||
<li v-else class="ide-search-list-empty justify-content-center gl-flex gl-items-center">
|
||||
{{ __('No merge requests found') }}
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
@ -1,22 +0,0 @@
|
||||
<script>
|
||||
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlIcon,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-icon
|
||||
v-gl-tooltip
|
||||
:title="__('Part of merge request changes')"
|
||||
:size="12"
|
||||
name="merge-request"
|
||||
class="gl-mr-3"
|
||||
/>
|
||||
</template>
|
@ -1,54 +0,0 @@
|
||||
<script>
|
||||
import $ from 'jquery';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapGetters } from 'vuex';
|
||||
import NavDropdownButton from './nav_dropdown_button.vue';
|
||||
import NavForm from './nav_form.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NavDropdownButton,
|
||||
NavForm,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isVisibleDropdown: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['canReadMergeRequests']),
|
||||
},
|
||||
mounted() {
|
||||
this.addDropdownListeners();
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.removeDropdownListeners();
|
||||
},
|
||||
methods: {
|
||||
addDropdownListeners() {
|
||||
$(this.$refs.dropdown)
|
||||
.on('show.bs.dropdown', () => this.showDropdown())
|
||||
.on('hide.bs.dropdown', () => this.hideDropdown());
|
||||
},
|
||||
removeDropdownListeners() {
|
||||
// eslint-disable-next-line @gitlab/no-global-event-off
|
||||
$(this.$refs.dropdown).off('show.bs.dropdown').off('hide.bs.dropdown');
|
||||
},
|
||||
showDropdown() {
|
||||
this.isVisibleDropdown = true;
|
||||
},
|
||||
hideDropdown() {
|
||||
this.isVisibleDropdown = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="dropdown" class="btn-group ide-nav-dropdown dropdown" data-testid="ide-nav-dropdown">
|
||||
<nav-dropdown-button :show-merge-requests="canReadMergeRequests" />
|
||||
<div class="dropdown-menu dropdown-menu-left p-0">
|
||||
<nav-form v-if="isVisibleDropdown" :show-merge-requests="canReadMergeRequests" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
@ -1,56 +0,0 @@
|
||||
<script>
|
||||
import { GlIcon } from '@gitlab/ui';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapState } from 'vuex';
|
||||
import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
|
||||
|
||||
const EMPTY_LABEL = '-';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlIcon,
|
||||
DropdownButton,
|
||||
},
|
||||
props: {
|
||||
showMergeRequests: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(['currentBranchId', 'currentMergeRequestId']),
|
||||
mergeRequestLabel() {
|
||||
return this.currentMergeRequestId ? `!${this.currentMergeRequestId}` : EMPTY_LABEL;
|
||||
},
|
||||
branchLabel() {
|
||||
return this.currentBranchId || EMPTY_LABEL;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<dropdown-button class="!gl-w-full">
|
||||
<span class="row gl-flex-nowrap">
|
||||
<span class="col-auto flex-fill text-truncate">
|
||||
<gl-icon
|
||||
data-testid="branch-icon"
|
||||
:size="16"
|
||||
:aria-label="__('Current Branch')"
|
||||
name="branch"
|
||||
/>
|
||||
{{ branchLabel }}
|
||||
</span>
|
||||
<span v-if="showMergeRequests" class="col-auto pl-0 text-truncate">
|
||||
<gl-icon
|
||||
data-testid="merge-request-icon"
|
||||
:size="16"
|
||||
:aria-label="__('Merge request')"
|
||||
name="merge-request"
|
||||
/>
|
||||
{{ mergeRequestLabel }}
|
||||
</span>
|
||||
</span>
|
||||
</dropdown-button>
|
||||
</template>
|
@ -1,35 +0,0 @@
|
||||
<script>
|
||||
import { GlTab, GlTabs } from '@gitlab/ui';
|
||||
import BranchesSearchList from './branches/search_list.vue';
|
||||
import MergeRequestSearchList from './merge_requests/list.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlTab,
|
||||
GlTabs,
|
||||
BranchesSearchList,
|
||||
MergeRequestSearchList,
|
||||
},
|
||||
props: {
|
||||
showMergeRequests: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ide-nav-form p-0">
|
||||
<gl-tabs v-if="showMergeRequests">
|
||||
<gl-tab :title="__('Branches')">
|
||||
<branches-search-list />
|
||||
</gl-tab>
|
||||
<gl-tab :title="__('Merge requests')">
|
||||
<merge-request-search-list />
|
||||
</gl-tab>
|
||||
</gl-tabs>
|
||||
<branches-search-list v-else />
|
||||
</div>
|
||||
</template>
|
@ -1,60 +0,0 @@
|
||||
<!-- eslint-disable vue/multi-word-component-names -->
|
||||
<script>
|
||||
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
components: {
|
||||
GlIcon,
|
||||
},
|
||||
props: {
|
||||
label: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
iconClasses: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
showLabel: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
tooltipTitle() {
|
||||
return this.showLabel ? '' : this.label;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
clicked() {
|
||||
this.$emit('click');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
v-gl-tooltip
|
||||
:aria-label="label"
|
||||
:title="tooltipTitle"
|
||||
type="button"
|
||||
class="gl-rounded-none gl-border-none !gl-bg-transparent gl-p-0 !gl-shadow-none !gl-outline-none"
|
||||
@click.stop.prevent="clicked"
|
||||
>
|
||||
<gl-icon :name="icon" :class="iconClasses" />
|
||||
<template v-if="showLabel">
|
||||
{{ label }}
|
||||
</template>
|
||||
</button>
|
||||
</template>
|
@ -1,118 +0,0 @@
|
||||
<!-- eslint-disable vue/multi-word-component-names -->
|
||||
<script>
|
||||
import { GlIcon } from '@gitlab/ui';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapActions } from 'vuex';
|
||||
import { modalTypes } from '../../constants';
|
||||
import ItemButton from './button.vue';
|
||||
import NewModal from './modal.vue';
|
||||
import Upload from './upload.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlIcon,
|
||||
Upload,
|
||||
ItemButton,
|
||||
NewModal,
|
||||
},
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
path: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
isOpen: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
isOpen() {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.dropdownMenu.scrollIntoView({
|
||||
block: 'nearest',
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['createTempEntry', 'deleteEntry']),
|
||||
createNewItem(type) {
|
||||
this.$refs.newModal.open(type, this.path);
|
||||
this.$emit('toggle', false);
|
||||
},
|
||||
openDropdown() {
|
||||
this.$emit('toggle', !this.isOpen);
|
||||
},
|
||||
},
|
||||
modalTypes,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ide-new-btn">
|
||||
<div
|
||||
:class="{
|
||||
show: isOpen,
|
||||
}"
|
||||
class="dropdown gl-flex"
|
||||
>
|
||||
<button
|
||||
:aria-label="__('Create new file or directory')"
|
||||
type="button"
|
||||
class="rounded border-0 ide-entry-dropdown-toggle gl-flex"
|
||||
@click.stop="openDropdown()"
|
||||
>
|
||||
<gl-icon name="ellipsis_v" />
|
||||
</button>
|
||||
<ul ref="dropdownMenu" class="dropdown-menu dropdown-menu-right" data-testid="dropdown-menu">
|
||||
<template v-if="type === 'tree'">
|
||||
<li>
|
||||
<item-button
|
||||
:label="__('New file')"
|
||||
class="gl-flex"
|
||||
icon="doc-new"
|
||||
icon-classes="mr-2"
|
||||
@click="createNewItem('blob')"
|
||||
/>
|
||||
</li>
|
||||
<upload :path="path" @create="createTempEntry" />
|
||||
<li>
|
||||
<item-button
|
||||
:label="__('New directory')"
|
||||
class="gl-flex"
|
||||
icon="folder-new"
|
||||
icon-classes="mr-2"
|
||||
@click="createNewItem($options.modalTypes.tree)"
|
||||
/>
|
||||
</li>
|
||||
<li class="divider"></li>
|
||||
</template>
|
||||
<li>
|
||||
<item-button
|
||||
:label="__('Rename/Move')"
|
||||
class="gl-flex"
|
||||
icon="pencil"
|
||||
icon-classes="mr-2"
|
||||
@click="createNewItem($options.modalTypes.rename)"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<item-button
|
||||
:label="__('Delete')"
|
||||
class="gl-flex"
|
||||
icon="remove"
|
||||
icon-classes="mr-2"
|
||||
@click="deleteEntry(path)"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<new-modal ref="newModal" />
|
||||
</div>
|
||||
</template>
|
@ -1,196 +0,0 @@
|
||||
<!-- eslint-disable vue/multi-word-component-names -->
|
||||
<script>
|
||||
import { GlModal, GlButton } from '@gitlab/ui';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapActions, mapState, mapGetters } from 'vuex';
|
||||
import { createAlert } from '~/alert';
|
||||
import { __, sprintf } from '~/locale';
|
||||
import { modalTypes } from '../../constants';
|
||||
import { trimPathComponents, getPathParent } from '../../utils';
|
||||
|
||||
const i18n = {
|
||||
cancelButtonText: __('Cancel'),
|
||||
};
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlModal,
|
||||
GlButton,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
entryName: '',
|
||||
modalType: modalTypes.blob,
|
||||
path: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(['entries']),
|
||||
...mapGetters('fileTemplates', ['templateTypes']),
|
||||
modalTitle() {
|
||||
const entry = this.entries[this.path];
|
||||
|
||||
if (this.modalType === modalTypes.tree) {
|
||||
return __('Create new directory');
|
||||
}
|
||||
if (this.modalType === modalTypes.rename) {
|
||||
return entry.type === modalTypes.tree ? __('Rename folder') : __('Rename file');
|
||||
}
|
||||
|
||||
return __('Create new file');
|
||||
},
|
||||
buttonLabel() {
|
||||
const entry = this.entries[this.path];
|
||||
|
||||
if (this.modalType === modalTypes.tree) {
|
||||
return __('Create directory');
|
||||
}
|
||||
if (this.modalType === modalTypes.rename) {
|
||||
return entry.type === modalTypes.tree ? __('Rename folder') : __('Rename file');
|
||||
}
|
||||
|
||||
return __('Create file');
|
||||
},
|
||||
actionPrimary() {
|
||||
return {
|
||||
text: this.buttonLabel,
|
||||
attributes: { variant: 'confirm' },
|
||||
};
|
||||
},
|
||||
actionCancel() {
|
||||
return {
|
||||
text: i18n.cancelButtonText,
|
||||
attributes: { variant: 'default' },
|
||||
};
|
||||
},
|
||||
isCreatingNewFile() {
|
||||
return this.modalType === modalTypes.blob;
|
||||
},
|
||||
placeholder() {
|
||||
return this.isCreatingNewFile ? 'dir/file_name' : 'dir/';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['createTempEntry', 'renameEntry']),
|
||||
submitAndClose() {
|
||||
this.submitForm();
|
||||
this.close();
|
||||
},
|
||||
submitForm() {
|
||||
this.entryName = trimPathComponents(this.entryName);
|
||||
|
||||
if (this.modalType === modalTypes.rename) {
|
||||
if (this.entries[this.entryName] && !this.entries[this.entryName].deleted) {
|
||||
createAlert({
|
||||
message: sprintf(__('The name "%{name}" is already taken in this directory.'), {
|
||||
name: this.entryName,
|
||||
}),
|
||||
fadeTransition: false,
|
||||
addBodyClass: true,
|
||||
});
|
||||
} else {
|
||||
let parentPath = this.entryName.split('/');
|
||||
const name = parentPath.pop();
|
||||
parentPath = parentPath.join('/');
|
||||
|
||||
this.renameEntry({
|
||||
path: this.path,
|
||||
name,
|
||||
parentPath,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.createTempEntry({
|
||||
name: this.entryName,
|
||||
type: this.modalType,
|
||||
});
|
||||
}
|
||||
},
|
||||
createFromTemplate(template) {
|
||||
const parent = getPathParent(this.entryName);
|
||||
const name = parent ? `${parent}/${template.name}` : template.name;
|
||||
this.createTempEntry({
|
||||
name,
|
||||
type: this.modalType,
|
||||
});
|
||||
|
||||
this.$refs.modal.toggle();
|
||||
},
|
||||
focusInput() {
|
||||
const name = this.entries[this.entryName]?.name;
|
||||
const inputValue = this.$refs.fieldName.value;
|
||||
|
||||
this.$refs.fieldName.focus();
|
||||
|
||||
if (name) {
|
||||
this.$refs.fieldName.setSelectionRange(inputValue.indexOf(name), inputValue.length);
|
||||
}
|
||||
},
|
||||
resetData() {
|
||||
this.entryName = '';
|
||||
this.path = '';
|
||||
this.modalType = modalTypes.blob;
|
||||
},
|
||||
open(type = modalTypes.blob, path = '') {
|
||||
this.modalType = type;
|
||||
this.path = path;
|
||||
|
||||
if (this.modalType === modalTypes.rename) {
|
||||
this.entryName = path;
|
||||
} else {
|
||||
this.entryName = path ? `${path}/` : '';
|
||||
}
|
||||
|
||||
this.$refs.modal.show();
|
||||
|
||||
// wait for modal to show first
|
||||
this.$nextTick(() => this.focusInput());
|
||||
},
|
||||
close() {
|
||||
this.$refs.modal.hide();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-modal
|
||||
ref="modal"
|
||||
modal-id="ide-new-entry"
|
||||
data-testid="ide-new-entry"
|
||||
:title="modalTitle"
|
||||
size="lg"
|
||||
:action-primary="actionPrimary"
|
||||
:action-cancel="actionCancel"
|
||||
@primary="submitForm"
|
||||
@cancel="resetData"
|
||||
>
|
||||
<div class="form-group row">
|
||||
<label class="label-bold col-form-label col-sm-2"> {{ __('Name') }} </label>
|
||||
<div class="col-sm-10">
|
||||
<form data-testid="file-name-form" @submit.prevent="submitAndClose">
|
||||
<input
|
||||
ref="fieldName"
|
||||
v-model.trim="entryName"
|
||||
type="text"
|
||||
class="form-control"
|
||||
data-testid="file-name-field"
|
||||
:placeholder="placeholder"
|
||||
/>
|
||||
</form>
|
||||
<ul v-if="isCreatingNewFile" class="file-templates list-inline gl-mt-3">
|
||||
<li v-for="(template, index) in templateTypes" :key="index" class="list-inline-item">
|
||||
<gl-button
|
||||
variant="dashed"
|
||||
category="secondary"
|
||||
class="p-1 pr-2 pl-2"
|
||||
@click="createFromTemplate(template)"
|
||||
>
|
||||
{{ template.name }}
|
||||
</gl-button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</gl-modal>
|
||||
</template>
|
@ -1,88 +0,0 @@
|
||||
<!-- eslint-disable vue/multi-word-component-names -->
|
||||
<script>
|
||||
import { isTextFile } from '~/ide/utils';
|
||||
import ItemButton from './button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ItemButton,
|
||||
},
|
||||
props: {
|
||||
path: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
showLabel: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
buttonCssClasses: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
createFile(target, file) {
|
||||
const { name, type: mimeType } = file;
|
||||
const encodedContent = target.result.split('base64,')[1];
|
||||
const rawContent = encodedContent ? atob(encodedContent) : '';
|
||||
const isText = isTextFile({ content: rawContent, mimeType, name });
|
||||
|
||||
const emitCreateEvent = (content) =>
|
||||
this.$emit('create', {
|
||||
name: `${this.path ? `${this.path}/` : ''}${name}`,
|
||||
type: 'blob',
|
||||
content,
|
||||
rawPath: !isText ? URL.createObjectURL(file) : '',
|
||||
mimeType,
|
||||
});
|
||||
|
||||
if (isText) {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.addEventListener('load', (e) => emitCreateEvent(e.target.result), { once: true });
|
||||
reader.readAsText(file);
|
||||
} else {
|
||||
emitCreateEvent(rawContent);
|
||||
}
|
||||
},
|
||||
readFile(file) {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.addEventListener('load', (e) => this.createFile(e.target, file), { once: true });
|
||||
reader.readAsDataURL(file);
|
||||
},
|
||||
openFile() {
|
||||
Array.from(this.$refs.fileUpload.files).forEach((file) => this.readFile(file));
|
||||
},
|
||||
startFileUpload() {
|
||||
this.$refs.fileUpload.click();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li>
|
||||
<item-button
|
||||
:class="buttonCssClasses"
|
||||
:show-label="showLabel"
|
||||
:icon-classes="showLabel ? 'mr-2' : ''"
|
||||
:label="__('Upload file')"
|
||||
class="gl-flex"
|
||||
icon="upload"
|
||||
@click="startFileUpload"
|
||||
/>
|
||||
<input
|
||||
id="file-upload"
|
||||
ref="fileUpload"
|
||||
type="file"
|
||||
class="hidden"
|
||||
multiple
|
||||
@change="openFile"
|
||||
/>
|
||||
</li>
|
||||
</template>
|
@ -1,101 +0,0 @@
|
||||
<script>
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapActions, mapState } from 'vuex';
|
||||
import IdeSidebarNav from '../ide_sidebar_nav.vue';
|
||||
|
||||
export default {
|
||||
name: 'CollapsibleSidebar',
|
||||
components: {
|
||||
IdeSidebarNav,
|
||||
},
|
||||
props: {
|
||||
extensionTabs: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
initOpenView: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
side: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
isOpen(state) {
|
||||
return state[this.namespace].isOpen;
|
||||
},
|
||||
currentView(state) {
|
||||
return state[this.namespace].currentView;
|
||||
},
|
||||
isAliveView(_state, getters) {
|
||||
return getters[`${this.namespace}/isAliveView`];
|
||||
},
|
||||
}),
|
||||
namespace() {
|
||||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||
return `${this.side}Pane`;
|
||||
},
|
||||
tabs() {
|
||||
return this.extensionTabs.filter((tab) => tab.show);
|
||||
},
|
||||
tabViews() {
|
||||
return this.tabs.map((tab) => tab.views).flat();
|
||||
},
|
||||
aliveTabViews() {
|
||||
return this.tabViews.filter((view) => this.isAliveView(view.name));
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.openViewByName(this.initOpenView);
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
toggleOpen(dispatch) {
|
||||
return dispatch(`${this.namespace}/toggleOpen`);
|
||||
},
|
||||
open(dispatch, view) {
|
||||
return dispatch(`${this.namespace}/open`, view);
|
||||
},
|
||||
}),
|
||||
openViewByName(viewName) {
|
||||
const view = viewName && this.tabViews.find((x) => x.name === viewName);
|
||||
|
||||
if (view) {
|
||||
this.open(view);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="`ide-${side}-sidebar`" class="multi-file-commit-panel ide-sidebar">
|
||||
<div
|
||||
v-show="isOpen"
|
||||
:class="`ide-${side}-sidebar-${currentView}`"
|
||||
class="multi-file-commit-panel-inner"
|
||||
>
|
||||
<div
|
||||
v-for="tabView in aliveTabViews"
|
||||
v-show="tabView.name === currentView"
|
||||
:key="tabView.name"
|
||||
class="flex-fill js-tab-view gl-h-full gl-overflow-hidden"
|
||||
>
|
||||
<component :is="tabView.component" />
|
||||
</div>
|
||||
</div>
|
||||
<ide-sidebar-nav
|
||||
:tabs="tabs"
|
||||
:side="side"
|
||||
:current-view="currentView"
|
||||
:is-open="isOpen"
|
||||
@open="open"
|
||||
@close="toggleOpen"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
@ -1,59 +0,0 @@
|
||||
<script>
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapState } from 'vuex';
|
||||
import { __ } from '~/locale';
|
||||
import { rightSidebarViews, SIDEBAR_INIT_WIDTH, SIDEBAR_NAV_WIDTH } from '../../constants';
|
||||
import JobsDetail from '../jobs/detail.vue';
|
||||
import PipelinesList from '../pipelines/list.vue';
|
||||
import ResizablePanel from '../resizable_panel.vue';
|
||||
import TerminalView from '../terminal/view.vue';
|
||||
import CollapsibleSidebar from './collapsible_sidebar.vue';
|
||||
|
||||
// Need to add the width of the nav buttons since the resizable container contains those as well
|
||||
const WIDTH = SIDEBAR_INIT_WIDTH + SIDEBAR_NAV_WIDTH;
|
||||
|
||||
export default {
|
||||
name: 'RightPane',
|
||||
components: {
|
||||
CollapsibleSidebar,
|
||||
ResizablePanel,
|
||||
},
|
||||
computed: {
|
||||
...mapState('terminal', { isTerminalVisible: 'isVisible' }),
|
||||
...mapState(['currentMergeRequestId']),
|
||||
...mapState('rightPane', ['isOpen']),
|
||||
rightExtensionTabs() {
|
||||
return [
|
||||
{
|
||||
show: true,
|
||||
title: __('Pipelines'),
|
||||
views: [
|
||||
{ component: PipelinesList, ...rightSidebarViews.pipelines },
|
||||
{ component: JobsDetail, ...rightSidebarViews.jobsDetail },
|
||||
],
|
||||
icon: 'rocket',
|
||||
},
|
||||
{
|
||||
show: this.isTerminalVisible,
|
||||
title: __('Terminal'),
|
||||
views: [{ component: TerminalView, ...rightSidebarViews.terminal }],
|
||||
icon: 'terminal',
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
WIDTH,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<resizable-panel
|
||||
class="gl-flex gl-overflow-hidden"
|
||||
side="right"
|
||||
:initial-width="$options.WIDTH"
|
||||
:min-size="$options.WIDTH"
|
||||
:resizable="isOpen"
|
||||
>
|
||||
<collapsible-sidebar class="gl-w-full" :extension-tabs="rightExtensionTabs" side="right" />
|
||||
</resizable-panel>
|
||||
</template>
|
@ -1,37 +0,0 @@
|
||||
<script>
|
||||
import { GlEmptyState } from '@gitlab/ui';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapState } from 'vuex';
|
||||
import { s__ } from '~/locale';
|
||||
import { helpPagePath } from '~/helpers/help_page_helper';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlEmptyState,
|
||||
},
|
||||
computed: {
|
||||
...mapState(['pipelinesEmptyStateSvgPath']),
|
||||
ciHelpPagePath() {
|
||||
return helpPagePath('ci/quick_start/_index.md');
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
title: s__('Pipelines|Build with confidence'),
|
||||
description: s__(`Pipelines|GitLab CI/CD can automatically build,
|
||||
test, and deploy your code. Let GitLab take care of time
|
||||
consuming tasks, so you can spend more time creating.`),
|
||||
primaryButtonText: s__('Pipelines|Get started with GitLab CI/CD'),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-empty-state
|
||||
:title="$options.i18n.title"
|
||||
:svg-path="pipelinesEmptyStateSvgPath"
|
||||
:svg-height="150"
|
||||
:description="$options.i18n.description"
|
||||
:primary-button-text="$options.i18n.primaryButtonText"
|
||||
:primary-button-link="ciHelpPagePath"
|
||||
/>
|
||||
</template>
|
@ -1,103 +0,0 @@
|
||||
<!-- eslint-disable vue/multi-word-component-names -->
|
||||
<script>
|
||||
import { GlLoadingIcon, GlIcon, GlTabs, GlTab, GlBadge, GlAlert } from '@gitlab/ui';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapActions, mapGetters, mapState } from 'vuex';
|
||||
import SafeHtml from '~/vue_shared/directives/safe_html';
|
||||
import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
|
||||
import JobsList from '../jobs/list.vue';
|
||||
import EmptyState from './empty_state.vue';
|
||||
|
||||
const CLASSES_FLEX_VERTICAL_CENTER = ['gl-h-full', 'gl-flex', 'gl-flex-col', 'gl-justify-center'];
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlIcon,
|
||||
CiIcon,
|
||||
JobsList,
|
||||
EmptyState,
|
||||
GlLoadingIcon,
|
||||
GlTabs,
|
||||
GlTab,
|
||||
GlBadge,
|
||||
GlAlert,
|
||||
},
|
||||
directives: {
|
||||
SafeHtml,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['currentProject']),
|
||||
...mapGetters('pipelines', ['jobsCount', 'failedJobsCount', 'failedStages', 'pipelineFailed']),
|
||||
...mapState('pipelines', [
|
||||
'isLoadingPipeline',
|
||||
'hasLoadedPipeline',
|
||||
'latestPipeline',
|
||||
'stages',
|
||||
'isLoadingJobs',
|
||||
]),
|
||||
showLoadingIcon() {
|
||||
return this.isLoadingPipeline && !this.hasLoadedPipeline;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.fetchLatestPipeline();
|
||||
},
|
||||
methods: {
|
||||
...mapActions('pipelines', ['fetchLatestPipeline']),
|
||||
},
|
||||
CLASSES_FLEX_VERTICAL_CENTER,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ide-pipeline">
|
||||
<div v-if="showLoadingIcon" :class="$options.CLASSES_FLEX_VERTICAL_CENTER">
|
||||
<gl-loading-icon size="lg" />
|
||||
</div>
|
||||
<template v-else-if="hasLoadedPipeline">
|
||||
<header v-if="latestPipeline" class="ide-tree-header ide-pipeline-header">
|
||||
<ci-icon :status="latestPipeline.details.status" />
|
||||
<span class="gl-ml-3">
|
||||
<strong> {{ __('Pipeline') }} </strong>
|
||||
<a
|
||||
:href="latestPipeline.path"
|
||||
target="_blank"
|
||||
class="ide-external-link position-relative"
|
||||
>
|
||||
#{{ latestPipeline.id }} <gl-icon :size="12" name="external-link" />
|
||||
</a>
|
||||
</span>
|
||||
</header>
|
||||
<div v-if="!latestPipeline" :class="$options.CLASSES_FLEX_VERTICAL_CENTER">
|
||||
<empty-state />
|
||||
</div>
|
||||
<gl-alert
|
||||
v-else-if="latestPipeline.yamlError"
|
||||
variant="danger"
|
||||
:dismissible="false"
|
||||
class="gl-mt-5"
|
||||
>
|
||||
<p class="gl-mb-0">{{ __('Unable to create pipeline') }}</p>
|
||||
<p class="break-word gl-mb-0">{{ latestPipeline.yamlError }}</p>
|
||||
</gl-alert>
|
||||
<gl-tabs v-else>
|
||||
<gl-tab :active="!pipelineFailed">
|
||||
<template #title>
|
||||
{{ __('Jobs') }}
|
||||
<gl-badge v-if="jobsCount" class="gl-tab-counter-badge">{{ jobsCount }}</gl-badge>
|
||||
</template>
|
||||
<jobs-list :loading="isLoadingJobs" :stages="stages" />
|
||||
</gl-tab>
|
||||
<gl-tab :active="pipelineFailed">
|
||||
<template #title>
|
||||
{{ __('Failed Jobs') }}
|
||||
<gl-badge v-if="failedJobsCount" class="gl-tab-counter-badge">{{
|
||||
failedJobsCount
|
||||
}}</gl-badge>
|
||||
</template>
|
||||
<jobs-list :loading="isLoadingJobs" :stages="failedStages" />
|
||||
</gl-tab>
|
||||
</gl-tabs>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
@ -1,72 +0,0 @@
|
||||
<script>
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapState, mapActions, mapGetters } from 'vuex';
|
||||
import { stageKeys } from '../constants';
|
||||
import EmptyState from './commit_sidebar/empty_state.vue';
|
||||
import CommitFilesList from './commit_sidebar/list.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CommitFilesList,
|
||||
EmptyState,
|
||||
},
|
||||
computed: {
|
||||
...mapState(['changedFiles', 'stagedFiles', 'lastCommitMsg']),
|
||||
...mapState('commit', ['commitMessage', 'submitCommitLoading']),
|
||||
...mapGetters(['lastOpenedFile', 'someUncommittedChanges', 'activeFile']),
|
||||
...mapGetters('commit', ['discardDraftButtonDisabled']),
|
||||
showStageUnstageArea() {
|
||||
return Boolean(this.someUncommittedChanges || this.lastCommitMsg);
|
||||
},
|
||||
activeFileKey() {
|
||||
return this.activeFile ? this.activeFile.key : null;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.initialize();
|
||||
},
|
||||
activated() {
|
||||
this.initialize();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['openPendingTab', 'updateViewer', 'updateActivityBarView']),
|
||||
initialize() {
|
||||
const file =
|
||||
this.lastOpenedFile && this.lastOpenedFile.type !== 'tree'
|
||||
? this.lastOpenedFile
|
||||
: this.activeFile;
|
||||
|
||||
if (!file) return;
|
||||
|
||||
this.openPendingTab({
|
||||
file,
|
||||
keyPrefix: file.staged ? stageKeys.staged : stageKeys.unstaged,
|
||||
})
|
||||
.then((changeViewer) => {
|
||||
if (changeViewer) {
|
||||
this.updateViewer('diff');
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
throw e;
|
||||
});
|
||||
},
|
||||
},
|
||||
stageKeys,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="multi-file-commit-panel-section">
|
||||
<template v-if="showStageUnstageArea">
|
||||
<commit-files-list
|
||||
:key-prefix="$options.stageKeys.staged"
|
||||
:file-list="stagedFiles"
|
||||
:active-file-key="activeFileKey"
|
||||
:empty-state-text="__('There are no changes')"
|
||||
class="is-first"
|
||||
/>
|
||||
</template>
|
||||
<empty-state v-else />
|
||||
</div>
|
||||
</template>
|
@ -1,540 +0,0 @@
|
||||
<script>
|
||||
import { GlTabs, GlTab } from '@gitlab/ui';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapState, mapGetters, mapActions } from 'vuex';
|
||||
import {
|
||||
EDITOR_TYPE_DIFF,
|
||||
EDITOR_TYPE_CODE,
|
||||
EDITOR_CODE_INSTANCE_FN,
|
||||
EDITOR_DIFF_INSTANCE_FN,
|
||||
} from '~/editor/constants';
|
||||
import { SourceEditorExtension } from '~/editor/extensions/source_editor_extension_base';
|
||||
import { EditorWebIdeExtension } from '~/editor/extensions/source_editor_webide_ext';
|
||||
import SourceEditor from '~/editor/source_editor';
|
||||
import { createAlert } from '~/alert';
|
||||
import ModelManager from '~/ide/lib/common/model_manager';
|
||||
import { defaultDiffEditorOptions, defaultEditorOptions } from '~/ide/lib/editor_options';
|
||||
import { __ } from '~/locale';
|
||||
import {
|
||||
WEBIDE_MARK_FILE_CLICKED,
|
||||
WEBIDE_MARK_REPO_EDITOR_START,
|
||||
WEBIDE_MARK_REPO_EDITOR_FINISH,
|
||||
WEBIDE_MEASURE_REPO_EDITOR,
|
||||
WEBIDE_MEASURE_FILE_AFTER_INTERACTION,
|
||||
} from '~/performance/constants';
|
||||
import { performanceMarkAndMeasure } from '~/performance/utils';
|
||||
import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
|
||||
import { viewerInformationForPath } from '~/vue_shared/components/content_viewer/lib/viewer_utils';
|
||||
import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
|
||||
import { markRaw } from '~/lib/utils/vue3compat/mark_raw';
|
||||
import { readFileAsDataURL } from '~/lib/utils/file_utility';
|
||||
import { hasCiConfigExtension } from '~/lib/utils/common_utils';
|
||||
|
||||
import {
|
||||
leftSidebarViews,
|
||||
viewerTypes,
|
||||
FILE_VIEW_MODE_EDITOR,
|
||||
FILE_VIEW_MODE_PREVIEW,
|
||||
} from '../constants';
|
||||
import eventHub from '../eventhub';
|
||||
import { getRulesWithTraversal } from '../lib/editorconfig/parser';
|
||||
import mapRulesToMonaco from '../lib/editorconfig/rules_mapper';
|
||||
import { getFileEditorOrDefault } from '../stores/modules/editor/utils';
|
||||
import { extractMarkdownImagesFromEntries } from '../stores/utils';
|
||||
import { getPathParent, registerSchema, isTextFile } from '../utils';
|
||||
import FileTemplatesBar from './file_templates/bar.vue';
|
||||
|
||||
const MARKDOWN_FILE_TYPE = 'markdown';
|
||||
|
||||
export default {
|
||||
name: 'RepoEditor',
|
||||
components: {
|
||||
GlTabs,
|
||||
GlTab,
|
||||
ContentViewer,
|
||||
DiffViewer,
|
||||
FileTemplatesBar,
|
||||
},
|
||||
props: {
|
||||
file: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
content: '',
|
||||
images: {},
|
||||
rules: {},
|
||||
globalEditor: null,
|
||||
modelManager: markRaw(new ModelManager()),
|
||||
isEditorLoading: true,
|
||||
unwatchCiYaml: null,
|
||||
SELivepreviewExtension: null,
|
||||
MarkdownLivePreview: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState('rightPane', {
|
||||
rightPaneIsOpen: 'isOpen',
|
||||
}),
|
||||
...mapState('editor', ['fileEditors']),
|
||||
...mapState([
|
||||
'viewer',
|
||||
'panelResizing',
|
||||
'currentActivityView',
|
||||
'renderWhitespaceInCode',
|
||||
'editorTheme',
|
||||
'entries',
|
||||
'currentProjectId',
|
||||
'previewMarkdownPath',
|
||||
]),
|
||||
...mapGetters([
|
||||
'currentMergeRequest',
|
||||
'getStagedFile',
|
||||
'isEditModeActive',
|
||||
'isCommitModeActive',
|
||||
'currentBranch',
|
||||
'getJsonSchemaForPath',
|
||||
]),
|
||||
...mapGetters('fileTemplates', ['showFileTemplatesBar']),
|
||||
fileEditor() {
|
||||
return getFileEditorOrDefault(this.fileEditors, this.file.path);
|
||||
},
|
||||
isBinaryFile() {
|
||||
return !isTextFile(this.file);
|
||||
},
|
||||
shouldHideEditor() {
|
||||
return this.file && !this.file.loading && this.isBinaryFile;
|
||||
},
|
||||
showContentViewer() {
|
||||
return (
|
||||
(this.shouldHideEditor || this.isPreviewViewMode) &&
|
||||
(this.viewer !== viewerTypes.mr || !this.file.mrChange)
|
||||
);
|
||||
},
|
||||
showDiffViewer() {
|
||||
return this.shouldHideEditor && this.file.mrChange && this.viewer === viewerTypes.mr;
|
||||
},
|
||||
isEditorViewMode() {
|
||||
return this.fileEditor.viewMode === FILE_VIEW_MODE_EDITOR;
|
||||
},
|
||||
isPreviewViewMode() {
|
||||
return this.fileEditor.viewMode === FILE_VIEW_MODE_PREVIEW;
|
||||
},
|
||||
showEditor() {
|
||||
return !this.shouldHideEditor && this.isEditorViewMode;
|
||||
},
|
||||
editorOptions() {
|
||||
return {
|
||||
renderWhitespace: this.renderWhitespaceInCode ? 'all' : 'none',
|
||||
theme: this.editorTheme,
|
||||
};
|
||||
},
|
||||
currentBranchCommit() {
|
||||
return this.currentBranch?.commit.id;
|
||||
},
|
||||
previewMode() {
|
||||
return viewerInformationForPath(this.file.path);
|
||||
},
|
||||
fileType() {
|
||||
return this.previewMode?.id || '';
|
||||
},
|
||||
showTabs() {
|
||||
return !this.shouldHideEditor && this.isEditModeActive && this.previewMode;
|
||||
},
|
||||
isCiConfigFile() {
|
||||
return (
|
||||
// For CI config schemas the filename must match '*.gitlab-ci.yml' regardless of project configuration.
|
||||
// https://gitlab.com/gitlab-org/gitlab/-/issues/293641
|
||||
hasCiConfigExtension(this.file.path) && this.editor?.getEditorType() === EDITOR_TYPE_CODE
|
||||
);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
file(newVal, oldVal) {
|
||||
if (oldVal.pending) {
|
||||
this.removePendingTab(oldVal);
|
||||
}
|
||||
|
||||
// Compare key to allow for files opened in review mode to be cached differently
|
||||
if (oldVal.key !== this.file.key) {
|
||||
this.isEditorLoading = true;
|
||||
this.initEditor();
|
||||
|
||||
if (this.currentActivityView !== leftSidebarViews.edit.name) {
|
||||
this.updateEditor({
|
||||
viewMode: FILE_VIEW_MODE_EDITOR,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
currentActivityView() {
|
||||
if (this.currentActivityView !== leftSidebarViews.edit.name) {
|
||||
this.updateEditor({
|
||||
viewMode: FILE_VIEW_MODE_EDITOR,
|
||||
});
|
||||
}
|
||||
},
|
||||
viewer() {
|
||||
this.isEditorLoading = false;
|
||||
if (!this.file.pending) {
|
||||
this.createEditorInstance();
|
||||
}
|
||||
},
|
||||
showContentViewer(val) {
|
||||
if (!val) return;
|
||||
|
||||
if (this.fileType === MARKDOWN_FILE_TYPE) {
|
||||
const { content, images } = extractMarkdownImagesFromEntries(this.file, this.entries);
|
||||
this.content = content;
|
||||
this.images = images;
|
||||
} else {
|
||||
this.content = this.file.content || this.file.raw;
|
||||
this.images = {};
|
||||
}
|
||||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.globalEditor.dispose();
|
||||
},
|
||||
mounted() {
|
||||
if (!this.globalEditor) {
|
||||
this.globalEditor = markRaw(new SourceEditor());
|
||||
}
|
||||
this.initEditor();
|
||||
|
||||
// listen in capture phase to be able to override Monaco's behaviour.
|
||||
window.addEventListener('paste', this.onPaste, true);
|
||||
},
|
||||
destroyed() {
|
||||
window.removeEventListener('paste', this.onPaste, true);
|
||||
},
|
||||
methods: {
|
||||
...mapActions([
|
||||
'getFileData',
|
||||
'getRawFileData',
|
||||
'changeFileContent',
|
||||
'removePendingTab',
|
||||
'triggerFilesChange',
|
||||
'addTempImage',
|
||||
]),
|
||||
...mapActions('editor', ['updateFileEditor']),
|
||||
initEditor() {
|
||||
performanceMarkAndMeasure({ mark: WEBIDE_MARK_REPO_EDITOR_START });
|
||||
if (this.shouldHideEditor && (this.file.content || this.file.raw)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Promise.all([this.fetchFileData(), this.fetchEditorconfigRules()])
|
||||
.then(() => {
|
||||
this.createEditorInstance();
|
||||
})
|
||||
.catch((err) => {
|
||||
createAlert({
|
||||
message: __('Error setting up editor. Please try again.'),
|
||||
fadeTransition: false,
|
||||
addBodyClass: true,
|
||||
});
|
||||
throw err;
|
||||
});
|
||||
},
|
||||
fetchFileData() {
|
||||
if (this.file.tempFile) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return this.getFileData({
|
||||
path: this.file.path,
|
||||
makeFileActive: false,
|
||||
toggleLoading: false,
|
||||
}).then(() =>
|
||||
this.getRawFileData({
|
||||
path: this.file.path,
|
||||
}),
|
||||
);
|
||||
},
|
||||
createEditorInstance() {
|
||||
if (this.isBinaryFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isDiff = this.viewer !== viewerTypes.edit;
|
||||
const shouldDisposeEditor = isDiff !== (this.editor?.getEditorType() === EDITOR_TYPE_DIFF);
|
||||
|
||||
if (this.editor && !shouldDisposeEditor) {
|
||||
this.setupEditor();
|
||||
} else {
|
||||
if (this.editor && shouldDisposeEditor) {
|
||||
this.editor.dispose();
|
||||
}
|
||||
const instanceOptions = isDiff ? defaultDiffEditorOptions : defaultEditorOptions;
|
||||
const method = isDiff ? EDITOR_DIFF_INSTANCE_FN : EDITOR_CODE_INSTANCE_FN;
|
||||
|
||||
this.editor = markRaw(
|
||||
this.globalEditor[method]({
|
||||
el: this.$refs.editor,
|
||||
blobPath: this.file.path,
|
||||
blobGlobalId: this.file.key,
|
||||
blobContent: this.content || this.file.content,
|
||||
...instanceOptions,
|
||||
...this.editorOptions,
|
||||
}),
|
||||
);
|
||||
this.editor.use([
|
||||
{
|
||||
definition: SourceEditorExtension,
|
||||
},
|
||||
{
|
||||
definition: EditorWebIdeExtension,
|
||||
setupOptions: {
|
||||
modelManager: this.modelManager,
|
||||
store: this.$store,
|
||||
file: this.file,
|
||||
options: this.editorOptions,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.setupEditor();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
setupEditor() {
|
||||
if (!this.file || !this.editor || this.file.loading) return;
|
||||
|
||||
const useLivePreviewExtension = () => {
|
||||
this.SELivepreviewExtension = this.editor.use({
|
||||
definition: this.MarkdownLivePreview,
|
||||
setupOptions: { previewMarkdownPath: this.previewMarkdownPath },
|
||||
});
|
||||
};
|
||||
if (
|
||||
this.fileType === MARKDOWN_FILE_TYPE &&
|
||||
this.editor?.getEditorType() === EDITOR_TYPE_CODE &&
|
||||
this.previewMarkdownPath
|
||||
) {
|
||||
if (this.MarkdownLivePreview) {
|
||||
useLivePreviewExtension();
|
||||
} else {
|
||||
import('~/editor/extensions/source_editor_markdown_livepreview_ext')
|
||||
.then(({ EditorMarkdownPreviewExtension }) => {
|
||||
this.MarkdownLivePreview = EditorMarkdownPreviewExtension;
|
||||
useLivePreviewExtension();
|
||||
})
|
||||
.catch((e) =>
|
||||
createAlert({
|
||||
message: e,
|
||||
}),
|
||||
);
|
||||
}
|
||||
} else if (this.SELivepreviewExtension) {
|
||||
this.editor.unuse(this.SELivepreviewExtension);
|
||||
}
|
||||
|
||||
const head = this.getStagedFile(this.file.path);
|
||||
|
||||
this.model = this.editor.createModel(
|
||||
this.file,
|
||||
this.file.staged && this.file.key.indexOf('unstaged-') === 0 ? head : null,
|
||||
);
|
||||
|
||||
if (this.viewer === viewerTypes.mr && this.file.mrChange) {
|
||||
this.editor.attachMergeRequestModel(this.model);
|
||||
} else {
|
||||
this.editor.attachModel(this.model);
|
||||
}
|
||||
|
||||
this.isEditorLoading = false;
|
||||
|
||||
this.model.updateOptions(this.rules);
|
||||
|
||||
this.registerSchemaForFile();
|
||||
|
||||
this.model.onChange((model) => {
|
||||
const { file } = model;
|
||||
if (!file.active) return;
|
||||
|
||||
const monacoModel = model.getModel();
|
||||
const content = monacoModel.getValue();
|
||||
this.changeFileContent({ path: file.path, content });
|
||||
});
|
||||
|
||||
// Handle Cursor Position
|
||||
this.editor.onPositionChange((instance, e) => {
|
||||
this.updateEditor({
|
||||
editorRow: e.position.lineNumber,
|
||||
editorColumn: e.position.column,
|
||||
});
|
||||
});
|
||||
|
||||
this.editor.setPos({
|
||||
lineNumber: this.fileEditor.editorRow,
|
||||
column: this.fileEditor.editorColumn,
|
||||
});
|
||||
|
||||
// Handle File Language
|
||||
this.updateEditor({
|
||||
fileLanguage: this.model.language,
|
||||
});
|
||||
|
||||
this.$emit('editorSetup');
|
||||
if (performance.getEntriesByName(WEBIDE_MARK_FILE_CLICKED).length) {
|
||||
eventHub.$emit(WEBIDE_MEASURE_FILE_AFTER_INTERACTION);
|
||||
} else {
|
||||
performanceMarkAndMeasure({
|
||||
mark: WEBIDE_MARK_REPO_EDITOR_FINISH,
|
||||
measures: [
|
||||
{
|
||||
name: WEBIDE_MEASURE_REPO_EDITOR,
|
||||
start: WEBIDE_MARK_REPO_EDITOR_START,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
},
|
||||
fetchEditorconfigRules() {
|
||||
return getRulesWithTraversal(this.file.path, (path) => {
|
||||
const entry = this.entries[path];
|
||||
if (!entry) return Promise.resolve(null);
|
||||
|
||||
const content = entry.content || entry.raw;
|
||||
if (content) return Promise.resolve(content);
|
||||
|
||||
return this.getFileData({ path: entry.path, makeFileActive: false }).then(() =>
|
||||
this.getRawFileData({ path: entry.path }),
|
||||
);
|
||||
}).then((rules) => {
|
||||
this.rules = mapRulesToMonaco(rules);
|
||||
});
|
||||
},
|
||||
onPaste(event) {
|
||||
const { editor } = this;
|
||||
const reImage = /^image\/(png|jpg|jpeg|gif)$/;
|
||||
const file = event.clipboardData.files[0];
|
||||
|
||||
if (
|
||||
editor.hasTextFocus() &&
|
||||
this.fileType === MARKDOWN_FILE_TYPE &&
|
||||
reImage.test(file?.type)
|
||||
) {
|
||||
// don't let the event be passed on to Monaco.
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
|
||||
return readFileAsDataURL(file).then((content) => {
|
||||
const parentPath = getPathParent(this.file.path);
|
||||
const path = `${parentPath ? `${parentPath}/` : ''}${file.name}`;
|
||||
|
||||
return this.addTempImage({
|
||||
name: path,
|
||||
rawPath: URL.createObjectURL(file),
|
||||
content: atob(content.split('base64,')[1]),
|
||||
}).then(({ name: fileName }) => {
|
||||
this.editor.replaceSelectedText(``);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// do nothing if no image is found in the clipboard
|
||||
return Promise.resolve();
|
||||
},
|
||||
registerSchemaForFile() {
|
||||
const registerExternalSchema = () => {
|
||||
const schema = this.getJsonSchemaForPath(this.file.path);
|
||||
return registerSchema(schema);
|
||||
};
|
||||
const registerLocalSchema = async () => {
|
||||
if (!this.CiSchemaExtension) {
|
||||
const { CiSchemaExtension } = await import(
|
||||
'~/editor/extensions/source_editor_ci_schema_ext'
|
||||
).catch((e) =>
|
||||
createAlert({
|
||||
message: e,
|
||||
}),
|
||||
);
|
||||
this.CiSchemaExtension = CiSchemaExtension;
|
||||
}
|
||||
this.editor.use({ definition: this.CiSchemaExtension });
|
||||
this.editor.registerCiSchema();
|
||||
};
|
||||
|
||||
if (this.isCiConfigFile) {
|
||||
registerLocalSchema();
|
||||
} else {
|
||||
if (this.CiSchemaExtension) {
|
||||
this.editor.unuse(this.CiSchemaExtension);
|
||||
}
|
||||
registerExternalSchema();
|
||||
}
|
||||
},
|
||||
updateEditor(data) {
|
||||
// Looks like our model wrapper `.dispose` causes the monaco editor to emit some position changes after
|
||||
// when disposing. We want to ignore these by only capturing editor changes that happen to the currently active
|
||||
// file.
|
||||
if (!this.file.active) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateFileEditor({ path: this.file.path, data });
|
||||
},
|
||||
},
|
||||
viewerTypes,
|
||||
FILE_VIEW_MODE_EDITOR,
|
||||
FILE_VIEW_MODE_PREVIEW,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="ide" class="blob-viewer-container blob-editor-container">
|
||||
<gl-tabs v-if="showTabs" content-class="gl-hidden">
|
||||
<gl-tab
|
||||
:title="__('Edit')"
|
||||
data-testid="edit-tab"
|
||||
@click="updateEditor({ viewMode: $options.FILE_VIEW_MODE_EDITOR })"
|
||||
/>
|
||||
<gl-tab
|
||||
:title="previewMode.previewTitle"
|
||||
data-testid="preview-tab"
|
||||
@click="updateEditor({ viewMode: $options.FILE_VIEW_MODE_PREVIEW })"
|
||||
/>
|
||||
</gl-tabs>
|
||||
<file-templates-bar v-else-if="showFileTemplatesBar(file.name)" />
|
||||
<div
|
||||
v-show="showEditor"
|
||||
ref="editor"
|
||||
:key="`content-editor`"
|
||||
:class="{
|
||||
'is-readonly': isCommitModeActive,
|
||||
'is-deleted': file.deleted,
|
||||
'is-added': file.tempFile,
|
||||
}"
|
||||
class="multi-file-editor-holder"
|
||||
data-testid="editor-container"
|
||||
:data-editor-loading="isEditorLoading"
|
||||
@focusout="triggerFilesChange"
|
||||
></div>
|
||||
<content-viewer
|
||||
v-if="showContentViewer"
|
||||
:content="content"
|
||||
:images="images"
|
||||
:path="file.rawPath || file.path"
|
||||
:file-path="file.path"
|
||||
:file-size="file.size"
|
||||
:project-path="currentProjectId"
|
||||
:commit-sha="currentBranchCommit"
|
||||
:type="fileType"
|
||||
/>
|
||||
<diff-viewer
|
||||
v-if="showDiffViewer"
|
||||
:diff-mode="file.mrChange.diffMode"
|
||||
:new-path="file.mrChange.new_path"
|
||||
:new-sha="currentMergeRequest.sha"
|
||||
:old-path="file.mrChange.old_path"
|
||||
:old-sha="currentMergeRequest.baseCommitSha"
|
||||
:project-path="currentProjectId"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
@ -1,33 +0,0 @@
|
||||
<script>
|
||||
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { __, sprintf } from '~/locale';
|
||||
import '~/lib/utils/datetime_utility';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlIcon,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
props: {
|
||||
file: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
lockTooltip() {
|
||||
return sprintf(__(`Locked by %{fileLockUserName}`), {
|
||||
fileLockUserName: this.file.file_lock.user.name,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span v-if="file.file_lock" v-gl-tooltip :title="lockTooltip" data-container="body">
|
||||
<gl-icon name="lock" class="file-status-icon" />
|
||||
</span>
|
||||
</template>
|
@ -1,103 +0,0 @@
|
||||
<script>
|
||||
import { GlIcon, GlTab } from '@gitlab/ui';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import { __, sprintf } from '~/locale';
|
||||
|
||||
import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
|
||||
import FileIcon from '~/vue_shared/components/file_icon.vue';
|
||||
import FileStatusIcon from './repo_file_status_icon.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FileStatusIcon,
|
||||
FileIcon,
|
||||
GlIcon,
|
||||
ChangedFileIcon,
|
||||
GlTab,
|
||||
},
|
||||
props: {
|
||||
tab: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tabMouseOver: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['getUrlForPath']),
|
||||
closeLabel() {
|
||||
if (this.fileHasChanged) {
|
||||
return sprintf(__('%{tabname} changed'), { tabname: this.tab.name });
|
||||
}
|
||||
return sprintf(__('Close %{tabname}'), { tabname: this.tab.name });
|
||||
},
|
||||
showChangedIcon() {
|
||||
if (this.tab.pending) return true;
|
||||
|
||||
return this.fileHasChanged ? !this.tabMouseOver : false;
|
||||
},
|
||||
fileHasChanged() {
|
||||
return this.tab.changed || this.tab.tempFile || this.tab.staged || this.tab.deleted;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapActions(['closeFile', 'updateDelayViewerUpdated', 'openPendingTab']),
|
||||
clickFile(tab) {
|
||||
if (tab.active) return;
|
||||
|
||||
this.updateDelayViewerUpdated(true);
|
||||
|
||||
if (tab.pending) {
|
||||
this.openPendingTab({ file: tab, keyPrefix: tab.staged ? 'staged' : 'unstaged' });
|
||||
} else {
|
||||
this.$router.push(this.getUrlForPath(tab.path));
|
||||
}
|
||||
},
|
||||
mouseOverTab() {
|
||||
if (this.fileHasChanged) {
|
||||
this.tabMouseOver = true;
|
||||
}
|
||||
},
|
||||
mouseOutTab() {
|
||||
if (this.fileHasChanged) {
|
||||
this.tabMouseOver = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-tab
|
||||
:active="tab.active"
|
||||
:disabled="tab.pending"
|
||||
:title="tab.name"
|
||||
@click="clickFile(tab)"
|
||||
@mouseover="mouseOverTab"
|
||||
@mouseout="mouseOutTab"
|
||||
>
|
||||
<template #title>
|
||||
<div :title="getUrlForPath(tab.path)" class="multi-file-tab">
|
||||
<file-icon :file-name="tab.name" :size="16" />
|
||||
{{ tab.name }}
|
||||
<file-status-icon :file="tab" />
|
||||
</div>
|
||||
<button
|
||||
:aria-label="closeLabel"
|
||||
:disabled="tab.pending"
|
||||
type="button"
|
||||
class="multi-file-tab-close"
|
||||
data-testid="close-button"
|
||||
@click.stop.prevent="closeFile(tab)"
|
||||
>
|
||||
<gl-icon v-if="!showChangedIcon" :size="12" name="close" />
|
||||
<changed-file-icon v-else :file="tab" />
|
||||
</button>
|
||||
</template>
|
||||
</gl-tab>
|
||||
</template>
|
@ -1,52 +0,0 @@
|
||||
<script>
|
||||
import { GlTabs } from '@gitlab/ui';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import RepoTab from './repo_tab.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
RepoTab,
|
||||
GlTabs,
|
||||
},
|
||||
props: {
|
||||
activeFile: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
files: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
viewer: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['getUrlForPath']),
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['updateViewer', 'removePendingTab']),
|
||||
openFileViewer(viewer) {
|
||||
this.updateViewer(viewer);
|
||||
|
||||
if (this.activeFile.pending) {
|
||||
return this.removePendingTab(this.activeFile).then(() => {
|
||||
this.$router.push(this.getUrlForPath(this.activeFile.path));
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="multi-file-tabs">
|
||||
<gl-tabs>
|
||||
<repo-tab v-for="tab in files" :key="tab.key" :tab="tab" />
|
||||
</gl-tabs>
|
||||
</div>
|
||||
</template>
|
@ -1,68 +0,0 @@
|
||||
<script>
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapActions } from 'vuex';
|
||||
import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
|
||||
import { SIDEBAR_MIN_WIDTH } from '../constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PanelResizer,
|
||||
},
|
||||
props: {
|
||||
initialWidth: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
minSize: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: SIDEBAR_MIN_WIDTH,
|
||||
},
|
||||
side: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
resizable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
width: this.initialWidth,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
panelStyle() {
|
||||
if (this.resizable) {
|
||||
return {
|
||||
width: `${this.width}px`,
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['setResizingStatus']),
|
||||
},
|
||||
maxSize: window.innerWidth / 2,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gl-relative" :style="panelStyle">
|
||||
<slot></slot>
|
||||
<panel-resizer
|
||||
v-show="resizable"
|
||||
:size.sync="width"
|
||||
:start-size="initialWidth"
|
||||
:min-size="minSize"
|
||||
:max-size="$options.maxSize"
|
||||
:side="side === 'right' ? 'left' : 'right'"
|
||||
@resize-start="setResizingStatus(true)"
|
||||
@resize-end="setResizingStatus(false)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
@ -1,106 +0,0 @@
|
||||
<script>
|
||||
import { GlIcon } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlIcon,
|
||||
},
|
||||
props: {
|
||||
placeholder: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: __('Search'),
|
||||
},
|
||||
tokens: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
backspaceCount: 0,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
placeholderText() {
|
||||
return this.tokens.length ? '' : this.placeholder;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
tokens() {
|
||||
this.$refs.input.focus();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onFocus() {
|
||||
this.$emit('focus');
|
||||
},
|
||||
onBlur() {
|
||||
this.$emit('blur');
|
||||
},
|
||||
onInput(evt) {
|
||||
this.$emit('input', evt.target.value);
|
||||
},
|
||||
onBackspace() {
|
||||
if (!this.value && this.tokens.length) {
|
||||
this.backspaceCount += 1;
|
||||
} else {
|
||||
this.backspaceCount = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.backspaceCount > 1) {
|
||||
this.removeToken(this.tokens[this.tokens.length - 1]);
|
||||
this.backspaceCount = 0;
|
||||
}
|
||||
},
|
||||
removeToken(token) {
|
||||
this.$emit('removeToken', token);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="filtered-search-wrapper">
|
||||
<div class="filtered-search-box">
|
||||
<div class="tokens-container list-unstyled">
|
||||
<div v-for="token in tokens" :key="token.label" class="filtered-search-token">
|
||||
<button
|
||||
class="selectable gl-rounded-none gl-border-none !gl-bg-transparent gl-p-0 !gl-shadow-none !gl-outline-none"
|
||||
type="button"
|
||||
@click.stop="removeToken(token)"
|
||||
@keyup.delete="removeToken(token)"
|
||||
>
|
||||
<div class="value-container rounded">
|
||||
<div class="value">{{ token.label }}</div>
|
||||
<div class="remove-token inverted">
|
||||
<gl-icon :size="16" name="close" />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="input-token">
|
||||
<input
|
||||
ref="input"
|
||||
:placeholder="placeholderText"
|
||||
:value="value"
|
||||
type="search"
|
||||
class="form-control filtered-search"
|
||||
@input="onInput"
|
||||
@focus="onFocus"
|
||||
@blur="onBlur"
|
||||
@keyup.delete="onBackspace"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
@ -1,73 +0,0 @@
|
||||
<script>
|
||||
import { GlLoadingIcon, GlButton, GlAlert } from '@gitlab/ui';
|
||||
import SafeHtml from '~/vue_shared/directives/safe_html';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlLoadingIcon,
|
||||
GlButton,
|
||||
GlAlert,
|
||||
},
|
||||
directives: {
|
||||
SafeHtml,
|
||||
},
|
||||
props: {
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
isValid: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
message: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
helpPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
illustrationPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onStart() {
|
||||
this.$emit('start');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="gl-p-5 gl-text-center">
|
||||
<div v-if="illustrationPath" class="svg-content svg-130"><img :src="illustrationPath" /></div>
|
||||
<h4>{{ __('Web Terminal') }}</h4>
|
||||
<gl-loading-icon v-if="isLoading" size="lg" class="gl-mt-3" />
|
||||
<template v-else>
|
||||
<p>{{ __('Run tests against your code live using the Web Terminal') }}</p>
|
||||
<p>
|
||||
<gl-button :disabled="!isValid" category="primary" variant="confirm" @click="onStart">
|
||||
{{ __('Start Web Terminal') }}
|
||||
</gl-button>
|
||||
</p>
|
||||
<gl-alert v-if="!isValid && message" variant="tip" :dismissible="false">
|
||||
<span v-safe-html="message"></span>
|
||||
</gl-alert>
|
||||
<p v-else>
|
||||
<a
|
||||
v-if="helpPath"
|
||||
:href="helpPath"
|
||||
target="_blank"
|
||||
v-text="__('Learn more about Web Terminal')"
|
||||
></a>
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
@ -1,57 +0,0 @@
|
||||
<!-- eslint-disable vue/multi-word-component-names -->
|
||||
<script>
|
||||
import { GlButton } from '@gitlab/ui';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapActions, mapState } from 'vuex';
|
||||
import { __ } from '~/locale';
|
||||
import { isEndingStatus } from '../../stores/modules/terminal/utils';
|
||||
import Terminal from './terminal.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Terminal,
|
||||
GlButton,
|
||||
},
|
||||
computed: {
|
||||
...mapState('terminal', ['session']),
|
||||
actionButton() {
|
||||
if (isEndingStatus(this.session.status)) {
|
||||
return {
|
||||
action: () => this.restartSession(),
|
||||
variant: 'confirm',
|
||||
category: 'primary',
|
||||
text: __('Restart Terminal'),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
action: () => this.stopSession(),
|
||||
variant: 'danger',
|
||||
category: 'secondary',
|
||||
text: __('Stop Terminal'),
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions('terminal', ['restartSession', 'stopSession']),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="session" class="ide-terminal flex-column gl-flex">
|
||||
<header class="ide-job-header gl-flex gl-items-center">
|
||||
<h5>{{ __('Web Terminal') }}</h5>
|
||||
<div class="ml-auto align-self-center">
|
||||
<gl-button
|
||||
v-if="actionButton"
|
||||
:variant="actionButton.variant"
|
||||
:category="actionButton.category"
|
||||
@click="actionButton.action"
|
||||
>{{ actionButton.text }}</gl-button
|
||||
>
|
||||
</div>
|
||||
</header>
|
||||
<terminal :terminal-path="session.terminalPath" :status="session.status" />
|
||||
</div>
|
||||
</template>
|
@ -1,119 +0,0 @@
|
||||
<!-- eslint-disable vue/multi-word-component-names -->
|
||||
<script>
|
||||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapState } from 'vuex';
|
||||
import { __ } from '~/locale';
|
||||
import GLTerminal from '~/terminal/terminal';
|
||||
import { RUNNING, STOPPING } from '../../stores/modules/terminal/constants';
|
||||
import { isStartingStatus } from '../../stores/modules/terminal/utils';
|
||||
import TerminalControls from './terminal_controls.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlLoadingIcon,
|
||||
TerminalControls,
|
||||
},
|
||||
props: {
|
||||
terminalPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
glterminal: null,
|
||||
canScrollUp: false,
|
||||
canScrollDown: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(['panelResizing']),
|
||||
loadingText() {
|
||||
if (isStartingStatus(this.status)) {
|
||||
return __('Starting...');
|
||||
}
|
||||
if (this.status === STOPPING) {
|
||||
return __('Stopping...');
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
panelResizing() {
|
||||
if (!this.panelResizing && this.glterminal) {
|
||||
this.glterminal.fit();
|
||||
}
|
||||
},
|
||||
status() {
|
||||
this.refresh();
|
||||
},
|
||||
terminalPath() {
|
||||
this.refresh();
|
||||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.destroyTerminal();
|
||||
},
|
||||
methods: {
|
||||
refresh() {
|
||||
if (this.status === RUNNING && this.terminalPath) {
|
||||
this.createTerminal();
|
||||
} else if (this.status === STOPPING) {
|
||||
this.stopTerminal();
|
||||
}
|
||||
},
|
||||
createTerminal() {
|
||||
this.destroyTerminal();
|
||||
this.glterminal = new GLTerminal(this.$refs.terminal);
|
||||
this.glterminal.addScrollListener(({ canScrollUp, canScrollDown }) => {
|
||||
this.canScrollUp = canScrollUp;
|
||||
this.canScrollDown = canScrollDown;
|
||||
});
|
||||
},
|
||||
destroyTerminal() {
|
||||
if (this.glterminal) {
|
||||
this.glterminal.dispose();
|
||||
this.glterminal = null;
|
||||
}
|
||||
},
|
||||
stopTerminal() {
|
||||
if (this.glterminal) {
|
||||
this.glterminal.disable();
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-column flex-fill min-height-0 pr-3 gl-flex">
|
||||
<div class="top-bar border-left-0 gl-flex gl-items-center">
|
||||
<div v-if="loadingText">
|
||||
<gl-loading-icon size="sm" :inline="true" />
|
||||
<span>{{ loadingText }}</span>
|
||||
</div>
|
||||
<terminal-controls
|
||||
v-if="glterminal"
|
||||
class="ml-auto"
|
||||
:can-scroll-up="canScrollUp"
|
||||
:can-scroll-down="canScrollDown"
|
||||
@scroll-up="glterminal.scrollToTop()"
|
||||
@scroll-down="glterminal.scrollToBottom()"
|
||||
/>
|
||||
</div>
|
||||
<div class="terminal-wrapper flex-fill min-height-0 gl-flex">
|
||||
<div
|
||||
ref="terminal"
|
||||
class="ide-terminal-trace flex-fill min-height-0 gl-w-full"
|
||||
:data-project-path="terminalPath"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
@ -1,27 +0,0 @@
|
||||
<script>
|
||||
import ScrollButton from '~/ide/components/jobs/detail/scroll_button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ScrollButton,
|
||||
},
|
||||
props: {
|
||||
canScrollUp: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
canScrollDown: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="controllers">
|
||||
<scroll-button :disabled="!canScrollUp" direction="up" @click="$emit('scroll-up')" />
|
||||
<scroll-button :disabled="!canScrollDown" direction="down" @click="$emit('scroll-down')" />
|
||||
</div>
|
||||
</template>
|
@ -1,42 +0,0 @@
|
||||
<!-- eslint-disable vue/multi-word-component-names -->
|
||||
<script>
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapActions, mapGetters, mapState } from 'vuex';
|
||||
import EmptyState from './empty_state.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EmptyState,
|
||||
TerminalSession: () => import(/* webpackChunkName: 'ide_terminal' */ './session.vue'),
|
||||
},
|
||||
computed: {
|
||||
...mapState('terminal', ['isShowSplash', 'paths']),
|
||||
...mapGetters('terminal', ['allCheck']),
|
||||
},
|
||||
methods: {
|
||||
...mapActions('terminal', ['startSession', 'hideSplash']),
|
||||
start() {
|
||||
this.startSession();
|
||||
this.hideSplash();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gl-h-full">
|
||||
<div v-if="isShowSplash" class="flex-column justify-content-center gl-flex gl-h-full">
|
||||
<empty-state
|
||||
:is-loading="allCheck.isLoading"
|
||||
:is-valid="allCheck.isValid"
|
||||
:message="allCheck.message"
|
||||
:help-path="paths.webTerminalHelpPath"
|
||||
:illustration-path="paths.webTerminalSvgPath"
|
||||
@start="start()"
|
||||
/>
|
||||
</div>
|
||||
<template v-else>
|
||||
<terminal-session />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
@ -1,80 +0,0 @@
|
||||
<script>
|
||||
import { GlTooltipDirective, GlLoadingIcon, GlIcon } from '@gitlab/ui';
|
||||
import { throttle } from 'lodash';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapState } from 'vuex';
|
||||
import {
|
||||
MSG_TERMINAL_SYNC_CONNECTING,
|
||||
MSG_TERMINAL_SYNC_UPLOADING,
|
||||
MSG_TERMINAL_SYNC_RUNNING,
|
||||
} from '../../stores/modules/terminal_sync/messages';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlIcon,
|
||||
GlLoadingIcon,
|
||||
},
|
||||
directives: {
|
||||
'gl-tooltip': GlTooltipDirective,
|
||||
},
|
||||
data() {
|
||||
return { isLoading: false };
|
||||
},
|
||||
computed: {
|
||||
...mapState('terminalSync', ['isError', 'isStarted', 'message']),
|
||||
...mapState('terminalSync', {
|
||||
isLoadingState: 'isLoading',
|
||||
}),
|
||||
status() {
|
||||
if (this.isLoading) {
|
||||
return {
|
||||
icon: '',
|
||||
text: this.isStarted ? MSG_TERMINAL_SYNC_UPLOADING : MSG_TERMINAL_SYNC_CONNECTING,
|
||||
};
|
||||
}
|
||||
if (this.isError) {
|
||||
return {
|
||||
icon: 'warning',
|
||||
text: this.message,
|
||||
};
|
||||
}
|
||||
if (this.isStarted) {
|
||||
return {
|
||||
icon: 'mobile-issue-close',
|
||||
text: MSG_TERMINAL_SYNC_RUNNING,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
// We want to throttle the `isLoading` updates so that
|
||||
// the user actually sees an indicator that changes are sent.
|
||||
isLoadingState: throttle(function watchIsLoadingState(val) {
|
||||
this.isLoading = val;
|
||||
}, 150),
|
||||
},
|
||||
created() {
|
||||
this.isLoading = this.isLoadingState;
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="status" v-gl-tooltip :title="status.text" role="note" class="gl-flex gl-items-center">
|
||||
<span>{{ __('Terminal') }}:</span>
|
||||
<span
|
||||
class="square s16 ml-1 gl-flex gl-items-center gl-justify-center"
|
||||
:aria-label="status.text"
|
||||
>
|
||||
<gl-loading-icon
|
||||
v-if="isLoading"
|
||||
inline
|
||||
size="sm"
|
||||
class="gl-flex gl-items-center gl-justify-center"
|
||||
/>
|
||||
<gl-icon v-else-if="status.icon" :name="status.icon" :size="16" />
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
@ -1,23 +0,0 @@
|
||||
<script>
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapState } from 'vuex';
|
||||
import TerminalSyncStatus from './terminal_sync_status.vue';
|
||||
|
||||
/**
|
||||
* It is possible that the vuex module is not registered.
|
||||
*
|
||||
* This component will gracefully handle this so the actual one can simply use `mapState(moduleName, ...)`.
|
||||
*/
|
||||
export default {
|
||||
components: {
|
||||
TerminalSyncStatus,
|
||||
},
|
||||
computed: {
|
||||
...mapState(['terminalSync']),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<terminal-sync-status v-if="terminalSync" />
|
||||
</template>
|
@ -1,35 +1,16 @@
|
||||
export const MAX_WINDOW_HEIGHT_COMPACT = 750;
|
||||
export const DEFAULT_BRANCH = 'main';
|
||||
|
||||
// Commit message textarea
|
||||
export const MAX_TITLE_LENGTH = 50;
|
||||
export const MAX_BODY_LENGTH = 72;
|
||||
export const GITLAB_WEB_IDE_FEEDBACK_ISSUE = 'https://gitlab.com/gitlab-org/gitlab/-/issues/377367';
|
||||
|
||||
export const SIDEBAR_INIT_WIDTH = 340;
|
||||
export const SIDEBAR_MIN_WIDTH = 260;
|
||||
export const SIDEBAR_NAV_WIDTH = 60;
|
||||
export const IDE_ELEMENT_ID = 'ide';
|
||||
|
||||
// File view modes
|
||||
export const FILE_VIEW_MODE_EDITOR = 'editor';
|
||||
export const FILE_VIEW_MODE_PREVIEW = 'preview';
|
||||
|
||||
export const PERMISSION_CREATE_MR = 'createMergeRequestIn';
|
||||
export const PERMISSION_READ_MR = 'readMergeRequest';
|
||||
export const PERMISSION_PUSH_CODE = 'pushCode';
|
||||
export const PUSH_RULE_REJECT_UNSIGNED_COMMITS = 'rejectUnsignedCommits';
|
||||
|
||||
// The default permission object to use when the project data isn't available yet.
|
||||
// This helps us encapsulate checks like `canPushCode` without requiring an
|
||||
// additional check like `currentProject && canPushCode`.
|
||||
export const DEFAULT_PERMISSIONS = {
|
||||
[PERMISSION_PUSH_CODE]: true,
|
||||
};
|
||||
|
||||
export const viewerTypes = {
|
||||
mr: 'mrdiff',
|
||||
edit: 'editor',
|
||||
diff: 'diff',
|
||||
};
|
||||
// note: This path comes from `config/routes.rb`
|
||||
export const IDE_PATH = '/-/ide';
|
||||
export const WEB_IDE_OAUTH_CALLBACK_URL_PATH = '/-/ide/oauth_redirect';
|
||||
|
||||
/**
|
||||
* LEGACY WEB IDE CONSTANTS USED BY OTHER FRONTEND FEATURES. DO NOT CONTINUE USING.
|
||||
*/
|
||||
export const diffModes = {
|
||||
replaced: 'replaced',
|
||||
new: 'new',
|
||||
@ -54,24 +35,6 @@ export const diffViewerErrors = Object.freeze({
|
||||
stored_externally: 'server_side_but_stored_externally',
|
||||
});
|
||||
|
||||
export const leftSidebarViews = {
|
||||
edit: { name: 'ide-tree' },
|
||||
review: { name: 'ide-review' },
|
||||
commit: { name: 'repo-commit-section' },
|
||||
};
|
||||
|
||||
export const rightSidebarViews = {
|
||||
pipelines: { name: 'pipelines-list', keepAlive: true },
|
||||
jobsDetail: { name: 'jobs-detail', keepAlive: false },
|
||||
mergeRequestInfo: { name: 'merge-request-info', keepAlive: true },
|
||||
terminal: { name: 'terminal', keepAlive: true },
|
||||
};
|
||||
|
||||
export const stageKeys = {
|
||||
unstaged: 'unstaged',
|
||||
staged: 'staged',
|
||||
};
|
||||
|
||||
export const commitItemIconMap = {
|
||||
addition: {
|
||||
icon: 'file-addition',
|
||||
@ -86,33 +49,3 @@ export const commitItemIconMap = {
|
||||
class: 'file-deletion ide-file-deletion',
|
||||
},
|
||||
};
|
||||
|
||||
export const modalTypes = {
|
||||
rename: 'rename',
|
||||
tree: 'tree',
|
||||
blob: 'blob',
|
||||
};
|
||||
|
||||
export const commitActionTypes = {
|
||||
move: 'move',
|
||||
delete: 'delete',
|
||||
create: 'create',
|
||||
update: 'update',
|
||||
};
|
||||
|
||||
export const SIDE_LEFT = 'left';
|
||||
export const SIDE_RIGHT = 'right';
|
||||
|
||||
// This is the maximum number of files to auto open when opening the Web IDE
|
||||
// from a merge request
|
||||
export const MAX_MR_FILES_AUTO_OPEN = 10;
|
||||
|
||||
export const DEFAULT_BRANCH = 'main';
|
||||
|
||||
export const GITLAB_WEB_IDE_FEEDBACK_ISSUE = 'https://gitlab.com/gitlab-org/gitlab/-/issues/377367';
|
||||
|
||||
export const IDE_ELEMENT_ID = 'ide';
|
||||
|
||||
// note: This path comes from `config/routes.rb`
|
||||
export const IDE_PATH = '/-/ide';
|
||||
export const WEB_IDE_OAUTH_CALLBACK_URL_PATH = '/-/ide/oauth_redirect';
|
||||
|
@ -1,3 +0,0 @@
|
||||
import createEventHub from '~/helpers/event_hub_factory';
|
||||
|
||||
export default createEventHub();
|
@ -1,112 +0,0 @@
|
||||
import Vue from 'vue';
|
||||
import IdeRouter from '~/ide/ide_router_extension';
|
||||
import { joinPaths } from '~/lib/utils/url_utility';
|
||||
import {
|
||||
WEBIDE_MARK_FETCH_PROJECT_DATA_START,
|
||||
WEBIDE_MARK_FETCH_PROJECT_DATA_FINISH,
|
||||
WEBIDE_MEASURE_FETCH_PROJECT_DATA,
|
||||
} from '~/performance/constants';
|
||||
import { performanceMarkAndMeasure } from '~/performance/utils';
|
||||
import { syncRouterAndStore } from './sync_router_and_store';
|
||||
|
||||
Vue.use(IdeRouter);
|
||||
|
||||
/**
|
||||
* Routes below /-/ide/:
|
||||
|
||||
/project/h5bp/html5-boilerplate/blob/main
|
||||
/project/h5bp/html5-boilerplate/blob/main/app/js/test.js
|
||||
|
||||
/project/h5bp/html5-boilerplate/mr/123
|
||||
/project/h5bp/html5-boilerplate/mr/123/app/js/test.js
|
||||
|
||||
/workspace/123
|
||||
/workspace/project/h5bp/html5-boilerplate/blob/my-special-branch
|
||||
/workspace/project/h5bp/html5-boilerplate/mr/123
|
||||
|
||||
/ = /workspace
|
||||
|
||||
/settings
|
||||
*/
|
||||
|
||||
// Unfortunately Vue Router doesn't work without at least a fake component
|
||||
// If you do only data handling
|
||||
export const EmptyRouterComponent = {
|
||||
render(createElement) {
|
||||
return createElement('div');
|
||||
},
|
||||
};
|
||||
|
||||
export const createRouter = (store, defaultBranch) => {
|
||||
const router = new IdeRouter({
|
||||
mode: 'history',
|
||||
base: joinPaths(gon.relative_url_root || '', '/-/ide/'),
|
||||
routes: [
|
||||
{
|
||||
path: '/project/:namespace+/:project',
|
||||
component: EmptyRouterComponent,
|
||||
children: [
|
||||
{
|
||||
path: ':targetmode(edit|tree|blob)/:branchid+/-/*',
|
||||
component: EmptyRouterComponent,
|
||||
},
|
||||
{
|
||||
path: ':targetmode(edit|tree|blob)/:branchid+/',
|
||||
redirect: (to) => joinPaths(to.path, '/-/'),
|
||||
},
|
||||
{
|
||||
path: ':targetmode(edit|tree|blob)',
|
||||
redirect: (to) => joinPaths(to.path, `/${defaultBranch}/-/`),
|
||||
},
|
||||
{
|
||||
path: 'merge_requests/:mrid',
|
||||
component: EmptyRouterComponent,
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
redirect: (to) => joinPaths(to.path, `/edit/${defaultBranch}/-/`),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
if (to.params.namespace && to.params.project) {
|
||||
const basePath = to.params.pathMatch || '';
|
||||
const projectId = `${to.params.namespace}/${to.params.project}`;
|
||||
const branchId = to.params.branchid;
|
||||
const mergeRequestId = to.params.mrid;
|
||||
|
||||
performanceMarkAndMeasure({ mark: WEBIDE_MARK_FETCH_PROJECT_DATA_START });
|
||||
if (branchId) {
|
||||
performanceMarkAndMeasure({
|
||||
mark: WEBIDE_MARK_FETCH_PROJECT_DATA_FINISH,
|
||||
measures: [
|
||||
{
|
||||
name: WEBIDE_MEASURE_FETCH_PROJECT_DATA,
|
||||
start: WEBIDE_MARK_FETCH_PROJECT_DATA_START,
|
||||
},
|
||||
],
|
||||
});
|
||||
store.dispatch('openBranch', {
|
||||
projectId,
|
||||
branchId,
|
||||
basePath,
|
||||
});
|
||||
} else if (mergeRequestId) {
|
||||
store.dispatch('openMergeRequest', {
|
||||
projectId,
|
||||
mergeRequestId,
|
||||
targetProjectId: to.query.target_project,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
syncRouterAndStore(router, store);
|
||||
|
||||
return router;
|
||||
};
|
@ -1,21 +0,0 @@
|
||||
import VueRouter from 'vue-router';
|
||||
import { escapeFileUrl } from '~/lib/utils/url_utility';
|
||||
|
||||
// To allow special characters (like "#," for example) in the branch names, we
|
||||
// should encode all the locations before those get processed by History API.
|
||||
// Otherwise, paths get messed up so that the router receives incorrect
|
||||
// branchid. The only way to do it consistently and in a more or less
|
||||
// future-proof manner is, unfortunately, to monkey-patch VueRouter or, as
|
||||
// suggested here, achieve the same more reliably by subclassing VueRouter and
|
||||
// update the methods, used in WebIDE.
|
||||
//
|
||||
// More context: https://gitlab.com/gitlab-org/gitlab/issues/35473
|
||||
|
||||
export default class IDERouter extends VueRouter {
|
||||
push(location, onComplete, onAbort) {
|
||||
super.push(escapeFileUrl(location), onComplete, onAbort);
|
||||
}
|
||||
resolve(to, current, append) {
|
||||
return super.resolve(escapeFileUrl(to), current, append);
|
||||
}
|
||||
}
|
@ -1,15 +1,10 @@
|
||||
import Vue from 'vue';
|
||||
import { IDE_ELEMENT_ID } from '~/ide/constants';
|
||||
import PerformancePlugin from '~/performance/vue_performance_plugin';
|
||||
import Translate from '~/vue_shared/translate';
|
||||
import { OAuthCallbackDomainMismatchErrorApp } from './oauth_callback_domain_mismatch_error';
|
||||
|
||||
Vue.use(Translate);
|
||||
|
||||
Vue.use(PerformancePlugin, {
|
||||
components: ['FileTree'],
|
||||
});
|
||||
|
||||
/**
|
||||
* Start the IDE.
|
||||
*
|
||||
|
@ -1,62 +0,0 @@
|
||||
// legacyWebIDE.js
|
||||
import { identity } from 'lodash';
|
||||
import Vue from 'vue';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapActions } from 'vuex';
|
||||
import { parseBoolean } from '../lib/utils/common_utils';
|
||||
import { DEFAULT_BRANCH } from './constants';
|
||||
import ide from './components/ide.vue';
|
||||
import { createRouter } from './ide_router';
|
||||
import { DEFAULT_THEME } from './lib/themes';
|
||||
import { createStore } from './stores';
|
||||
|
||||
export const initLegacyWebIDE = (el, options = {}) => {
|
||||
if (!el) return null;
|
||||
|
||||
const { rootComponent = ide, extendStore = identity } = options;
|
||||
|
||||
const store = createStore();
|
||||
const project = JSON.parse(el.dataset.project);
|
||||
store.dispatch('setProject', { project });
|
||||
|
||||
// fire and forget fetching non-critical project info
|
||||
store.dispatch('fetchProjectPermissions');
|
||||
|
||||
const router = createRouter(store, el.dataset.defaultBranch || DEFAULT_BRANCH);
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
store: extendStore(store, el),
|
||||
router,
|
||||
created() {
|
||||
this.setEmptyStateSvgs({
|
||||
emptyStateSvgPath: el.dataset.emptyStateSvgPath,
|
||||
noChangesStateSvgPath: el.dataset.noChangesStateSvgPath,
|
||||
committedStateSvgPath: el.dataset.committedStateSvgPath,
|
||||
pipelinesEmptyStateSvgPath: el.dataset.pipelinesEmptyStateSvgPath,
|
||||
switchEditorSvgPath: el.dataset.switchEditorSvgPath,
|
||||
});
|
||||
this.setLinks({
|
||||
webIDEHelpPagePath: el.dataset.webIdeHelpPagePath,
|
||||
newWebIDEHelpPagePath: el.dataset.newWebIdeHelpPagePath,
|
||||
forkInfo: el.dataset.forkInfo ? JSON.parse(el.dataset.forkInfo) : null,
|
||||
});
|
||||
this.init({
|
||||
renderWhitespaceInCode: parseBoolean(el.dataset.renderWhitespaceInCode),
|
||||
editorTheme: window.gon?.user_color_scheme || DEFAULT_THEME,
|
||||
previewMarkdownPath: el.dataset.previewMarkdownPath,
|
||||
userPreferencesPath: el.dataset.userPreferencesPath,
|
||||
});
|
||||
},
|
||||
beforeDestroy() {
|
||||
// This helps tests do Singleton cleanups which we don't really have responsibility to know about here.
|
||||
this.$emit('destroy');
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['setEmptyStateSvgs', 'setLinks', 'init']),
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement(rootComponent);
|
||||
},
|
||||
});
|
||||
};
|
@ -1,14 +0,0 @@
|
||||
export default class Disposable {
|
||||
constructor() {
|
||||
this.disposers = new Set();
|
||||
}
|
||||
|
||||
add(...disposers) {
|
||||
disposers.forEach((disposer) => this.disposers.add(disposer));
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.disposers.forEach((disposer) => disposer.dispose());
|
||||
this.disposers.clear();
|
||||
}
|
||||
}
|
@ -1,136 +0,0 @@
|
||||
import { editor as monacoEditor, Uri } from 'monaco-editor';
|
||||
import { insertFinalNewline } from '~/lib/utils/text_utility';
|
||||
import eventHub from '../../eventhub';
|
||||
import { trimTrailingWhitespace } from '../../utils';
|
||||
import { defaultModelOptions } from '../editor_options';
|
||||
import Disposable from './disposable';
|
||||
|
||||
export default class Model {
|
||||
constructor(file, head = null) {
|
||||
this.disposable = new Disposable();
|
||||
this.file = file;
|
||||
this.head = head;
|
||||
this.content = file.content !== '' || file.deleted ? file.content : file.raw;
|
||||
this.options = { ...defaultModelOptions };
|
||||
|
||||
this.disposable.add(
|
||||
(this.originalModel = monacoEditor.createModel(
|
||||
head ? head.content : this.file.raw,
|
||||
undefined,
|
||||
new Uri('gitlab', false, `original/${this.path}`),
|
||||
)),
|
||||
(this.model = monacoEditor.createModel(
|
||||
this.content,
|
||||
undefined,
|
||||
new Uri('gitlab', false, this.path),
|
||||
)),
|
||||
);
|
||||
if (this.file.mrChange) {
|
||||
this.disposable.add(
|
||||
(this.baseModel = monacoEditor.createModel(
|
||||
this.file.baseRaw,
|
||||
undefined,
|
||||
new Uri('gitlab', false, `target/${this.path}`),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
this.events = new Set();
|
||||
|
||||
this.updateContent = this.updateContent.bind(this);
|
||||
this.updateNewContent = this.updateNewContent.bind(this);
|
||||
this.dispose = this.dispose.bind(this);
|
||||
|
||||
eventHub.$on(`editor.update.model.dispose.${this.file.key}`, this.dispose);
|
||||
eventHub.$on(`editor.update.model.content.${this.file.key}`, this.updateContent);
|
||||
eventHub.$on(`editor.update.model.new.content.${this.file.key}`, this.updateNewContent);
|
||||
}
|
||||
|
||||
get url() {
|
||||
return this.model.uri.toString();
|
||||
}
|
||||
|
||||
get language() {
|
||||
return this.model.getLanguageId();
|
||||
}
|
||||
|
||||
get path() {
|
||||
return this.file.key;
|
||||
}
|
||||
|
||||
getModel() {
|
||||
return this.model;
|
||||
}
|
||||
|
||||
getOriginalModel() {
|
||||
return this.originalModel;
|
||||
}
|
||||
|
||||
getBaseModel() {
|
||||
return this.baseModel;
|
||||
}
|
||||
|
||||
setValue(value) {
|
||||
this.getModel().setValue(value);
|
||||
}
|
||||
|
||||
onChange(cb) {
|
||||
this.events.add(this.disposable.add(this.model.onDidChangeContent((e) => cb(this, e))));
|
||||
}
|
||||
|
||||
onDispose(cb) {
|
||||
this.events.add(cb);
|
||||
}
|
||||
|
||||
updateContent({ content, changed }) {
|
||||
this.getOriginalModel().setValue(content);
|
||||
|
||||
if (!changed) {
|
||||
this.getModel().setValue(content);
|
||||
}
|
||||
}
|
||||
|
||||
updateNewContent(content) {
|
||||
this.getModel().setValue(content);
|
||||
}
|
||||
|
||||
updateOptions(obj = {}) {
|
||||
Object.assign(this.options, obj);
|
||||
this.model.updateOptions(obj);
|
||||
this.applyCustomOptions();
|
||||
}
|
||||
|
||||
applyCustomOptions() {
|
||||
this.updateNewContent(
|
||||
Object.entries(this.options).reduce((content, [key, value]) => {
|
||||
switch (key) {
|
||||
case 'endOfLine':
|
||||
this.model.pushEOL(value);
|
||||
return this.model.getValue();
|
||||
case 'insertFinalNewline':
|
||||
return value ? insertFinalNewline(content) : content;
|
||||
case 'trimTrailingWhitespace':
|
||||
return value ? trimTrailingWhitespace(content) : content;
|
||||
default:
|
||||
return content;
|
||||
}
|
||||
}, this.model.getValue()),
|
||||
);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
if (!this.model.isDisposed()) this.applyCustomOptions();
|
||||
|
||||
this.events.forEach((cb) => {
|
||||
if (typeof cb === 'function') cb();
|
||||
});
|
||||
|
||||
this.events.clear();
|
||||
|
||||
eventHub.$off(`editor.update.model.dispose.${this.file.key}`, this.dispose);
|
||||
eventHub.$off(`editor.update.model.content.${this.file.key}`, this.updateContent);
|
||||
eventHub.$off(`editor.update.model.new.content.${this.file.key}`, this.updateNewContent);
|
||||
|
||||
this.disposable.dispose();
|
||||
}
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
import eventHub from '../../eventhub';
|
||||
import Disposable from './disposable';
|
||||
import Model from './model';
|
||||
|
||||
export default class ModelManager {
|
||||
constructor() {
|
||||
this.disposable = new Disposable();
|
||||
this.models = new Map();
|
||||
}
|
||||
|
||||
hasCachedModel(key) {
|
||||
return this.models.has(key);
|
||||
}
|
||||
|
||||
getModel(key) {
|
||||
return this.models.get(key);
|
||||
}
|
||||
|
||||
addModel(file, head = null) {
|
||||
if (this.hasCachedModel(file.key)) {
|
||||
return this.getModel(file.key);
|
||||
}
|
||||
|
||||
const model = new Model(file, head);
|
||||
this.models.set(model.path, model);
|
||||
this.disposable.add(model);
|
||||
|
||||
eventHub.$on(
|
||||
`editor.update.model.dispose.${file.key}`,
|
||||
this.removeCachedModel.bind(this, file),
|
||||
);
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
removeCachedModel(file) {
|
||||
this.models.delete(file.key);
|
||||
|
||||
eventHub.$off(`editor.update.model.dispose.${file.key}`, this.removeCachedModel);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
// dispose of all the models
|
||||
this.disposable.dispose();
|
||||
this.models.clear();
|
||||
}
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
import { commitActionTypes } from '~/ide/constants';
|
||||
import { commitActionForFile } from '~/ide/stores/utils';
|
||||
import createFileDiff from './create_file_diff';
|
||||
|
||||
const getDeletedParents = (entries, file) => {
|
||||
const parent = file.parentPath && entries[file.parentPath];
|
||||
|
||||
if (parent && parent.deleted) {
|
||||
return [parent, ...getDeletedParents(entries, parent)];
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
const filesWithChanges = ({ stagedFiles = [], changedFiles = [], entries = {} }) => {
|
||||
// We need changed files to overwrite staged, so put them at the end.
|
||||
const changes = stagedFiles.concat(changedFiles).reduce((acc, file) => {
|
||||
const key = file.path;
|
||||
const action = commitActionForFile(file);
|
||||
const prev = acc[key];
|
||||
|
||||
// If a file was deleted, which was previously added, then we should do nothing.
|
||||
if (action === commitActionTypes.delete && prev && prev.action === commitActionTypes.create) {
|
||||
delete acc[key];
|
||||
} else {
|
||||
acc[key] = { action, file };
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// We need to clean "move" actions, because we can only support 100% similarity moves at the moment.
|
||||
// This is because the previous file's content might not be loaded.
|
||||
Object.values(changes)
|
||||
.filter((change) => change.action === commitActionTypes.move)
|
||||
.forEach((change) => {
|
||||
const prev = changes[change.file.prevPath];
|
||||
|
||||
if (!prev) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (change.file.content === prev.file.content) {
|
||||
// If content is the same, continue with the move but don't do the prevPath's delete.
|
||||
delete changes[change.file.prevPath];
|
||||
} else {
|
||||
// Otherwise, treat the move as a delete / create.
|
||||
Object.assign(change, { action: commitActionTypes.create });
|
||||
}
|
||||
});
|
||||
|
||||
// Next, we need to add deleted directories by looking at the parents
|
||||
Object.values(changes)
|
||||
.filter((change) => change.action === commitActionTypes.delete && change.file.parentPath)
|
||||
.forEach(({ file }) => {
|
||||
// Do nothing if we've already visited this directory.
|
||||
if (changes[file.parentPath]) {
|
||||
return;
|
||||
}
|
||||
|
||||
getDeletedParents(entries, file).forEach((parent) => {
|
||||
changes[parent.path] = { action: commitActionTypes.delete, file: parent };
|
||||
});
|
||||
});
|
||||
|
||||
return Object.values(changes);
|
||||
};
|
||||
|
||||
const createDiff = (state) => {
|
||||
const changes = filesWithChanges(state);
|
||||
|
||||
const toDelete = changes
|
||||
.filter((x) => x.action === commitActionTypes.delete)
|
||||
.map((x) => x.file.path);
|
||||
|
||||
const patch = changes
|
||||
.filter((x) => x.action !== commitActionTypes.delete)
|
||||
.map(({ file, action }) => createFileDiff(file, action))
|
||||
.join('');
|
||||
|
||||
return {
|
||||
patch,
|
||||
toDelete,
|
||||
};
|
||||
};
|
||||
|
||||
export default createDiff;
|
@ -1,112 +0,0 @@
|
||||
/* eslint-disable @gitlab/require-i18n-strings */
|
||||
import { createTwoFilesPatch } from 'diff';
|
||||
import { commitActionTypes } from '~/ide/constants';
|
||||
|
||||
const DEV_NULL = '/dev/null';
|
||||
const DEFAULT_MODE = '100644';
|
||||
const NO_NEW_LINE = '\\ No newline at end of file';
|
||||
const NEW_LINE = '\n';
|
||||
|
||||
/**
|
||||
* Cleans patch generated by `diff` package.
|
||||
*
|
||||
* - Removes "=======" separator added at the beginning
|
||||
*/
|
||||
const cleanTwoFilesPatch = (text) => text.replace(/^(=+\s*)/, '');
|
||||
|
||||
const endsWithNewLine = (val) => !val || val[val.length - 1] === NEW_LINE;
|
||||
|
||||
const addEndingNewLine = (val) => (endsWithNewLine(val) ? val : val + NEW_LINE);
|
||||
|
||||
const removeEndingNewLine = (val) => (endsWithNewLine(val) ? val.substr(0, val.length - 1) : val);
|
||||
|
||||
const diffHead = (prevPath, newPath = '') =>
|
||||
`diff --git "a/${prevPath}" "b/${newPath || prevPath}"`;
|
||||
|
||||
const createDiffBody = (path, content, isCreate) => {
|
||||
if (!content) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const prefix = isCreate ? '+' : '-';
|
||||
const fromPath = isCreate ? DEV_NULL : `a/${path}`;
|
||||
const toPath = isCreate ? `b/${path}` : DEV_NULL;
|
||||
|
||||
const hasNewLine = endsWithNewLine(content);
|
||||
const lines = removeEndingNewLine(content).split(NEW_LINE);
|
||||
|
||||
const chunkHead = isCreate ? `@@ -0,0 +1,${lines.length} @@` : `@@ -1,${lines.length} +0,0 @@`;
|
||||
const chunk = lines
|
||||
.map((line) => `${prefix}${line}`)
|
||||
.concat(!hasNewLine ? [NO_NEW_LINE] : [])
|
||||
.join(NEW_LINE);
|
||||
|
||||
return `--- ${fromPath}
|
||||
+++ ${toPath}
|
||||
${chunkHead}
|
||||
${chunk}`;
|
||||
};
|
||||
|
||||
const createMoveFileDiff = (prevPath, newPath) => `${diffHead(prevPath, newPath)}
|
||||
rename from ${prevPath}
|
||||
rename to ${newPath}`;
|
||||
|
||||
const createNewFileDiff = (path, content) => {
|
||||
const diff = createDiffBody(path, content, true);
|
||||
|
||||
return `${diffHead(path)}
|
||||
new file mode ${DEFAULT_MODE}
|
||||
${diff}`;
|
||||
};
|
||||
|
||||
const createDeleteFileDiff = (path, content) => {
|
||||
const diff = createDiffBody(path, content, false);
|
||||
|
||||
return `${diffHead(path)}
|
||||
deleted file mode ${DEFAULT_MODE}
|
||||
${diff}`;
|
||||
};
|
||||
|
||||
const createUpdateFileDiff = (path, oldContent, newContent) => {
|
||||
const patch = createTwoFilesPatch(`a/${path}`, `b/${path}`, oldContent, newContent);
|
||||
|
||||
return `${diffHead(path)}
|
||||
${cleanTwoFilesPatch(patch)}`;
|
||||
};
|
||||
|
||||
const createFileDiffRaw = (file, action) => {
|
||||
switch (action) {
|
||||
case commitActionTypes.move:
|
||||
return createMoveFileDiff(file.prevPath, file.path);
|
||||
case commitActionTypes.create:
|
||||
return createNewFileDiff(file.path, file.content);
|
||||
case commitActionTypes.delete:
|
||||
return createDeleteFileDiff(file.path, file.content);
|
||||
case commitActionTypes.update:
|
||||
return createUpdateFileDiff(file.path, file.raw || '', file.content);
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a git diff for a single IDE file.
|
||||
*
|
||||
* ## Notes:
|
||||
* When called with `commitActionType.move`, it assumes that the move
|
||||
* is a 100% similarity move. No diff will be generated. This is because
|
||||
* generating a move with changes is not support by the current IDE, since
|
||||
* the source file might not have it's content loaded yet.
|
||||
*
|
||||
* When called with `commitActionType.delete`, it does not support
|
||||
* deleting files with a mode different than 100644. For the IDE mirror, this
|
||||
* isn't needed because deleting is handled outside the unified patch.
|
||||
*
|
||||
* ## References:
|
||||
* - https://git-scm.com/docs/git-diff#_generating_patches_with_p
|
||||
*/
|
||||
const createFileDiff = (file, action) =>
|
||||
// It's important that the file diff ends in a new line - git expects this.
|
||||
addEndingNewLine(createFileDiffRaw(file, action));
|
||||
|
||||
export default createFileDiff;
|
@ -1,54 +0,0 @@
|
||||
export default class DecorationsController {
|
||||
constructor(editor) {
|
||||
this.editor = editor;
|
||||
this.decorations = new Map();
|
||||
this.editorDecorations = new Map();
|
||||
}
|
||||
|
||||
getAllDecorationsForModel(model) {
|
||||
if (!this.decorations.has(model.url)) return [];
|
||||
|
||||
const modelDecorations = this.decorations.get(model.url);
|
||||
const decorations = [];
|
||||
|
||||
modelDecorations.forEach((val) => decorations.push(...val));
|
||||
|
||||
return decorations;
|
||||
}
|
||||
|
||||
addDecorations(model, decorationsKey, decorations) {
|
||||
const decorationMap = this.decorations.get(model.url) || new Map();
|
||||
|
||||
decorationMap.set(decorationsKey, decorations);
|
||||
|
||||
this.decorations.set(model.url, decorationMap);
|
||||
|
||||
this.decorate(model);
|
||||
}
|
||||
|
||||
decorate(model) {
|
||||
if (!this.editor.instance) return;
|
||||
|
||||
const decorations = this.getAllDecorationsForModel(model);
|
||||
const oldDecorations = this.editorDecorations.get(model.url) || [];
|
||||
|
||||
this.editorDecorations.set(
|
||||
model.url,
|
||||
this.editor.instance.deltaDecorations(oldDecorations, decorations),
|
||||
);
|
||||
}
|
||||
|
||||
hasDecorations(model) {
|
||||
return this.decorations.has(model.url);
|
||||
}
|
||||
|
||||
removeDecorations(model) {
|
||||
this.decorations.delete(model.url);
|
||||
this.editorDecorations.delete(model.url);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.decorations.clear();
|
||||
this.editorDecorations.clear();
|
||||
}
|
||||
}
|
@ -1,89 +0,0 @@
|
||||
import { throttle } from 'lodash';
|
||||
import { Range } from 'monaco-editor';
|
||||
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
|
||||
import Disposable from '../common/disposable';
|
||||
import DirtyDiffWorker from './diff_worker?worker';
|
||||
|
||||
export const getDiffChangeType = (change) => {
|
||||
if (change.modified) {
|
||||
return 'modified';
|
||||
}
|
||||
if (change.added) {
|
||||
return 'added';
|
||||
}
|
||||
if (change.removed) {
|
||||
return 'removed';
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
export const getDecorator = (change) => ({
|
||||
range: new Range(change.lineNumber, 1, change.endLineNumber, 1),
|
||||
options: {
|
||||
isWholeLine: true,
|
||||
linesDecorationsClassName: `dirty-diff dirty-diff-${getDiffChangeType(change)}`,
|
||||
},
|
||||
});
|
||||
|
||||
export default class DirtyDiffController {
|
||||
constructor(modelManager, decorationsController) {
|
||||
this.disposable = new Disposable();
|
||||
this.models = new Map();
|
||||
this.editorSimpleWorker = null;
|
||||
this.modelManager = modelManager;
|
||||
this.decorationsController = decorationsController;
|
||||
this.dirtyDiffWorker = new DirtyDiffWorker();
|
||||
this.throttledComputeDiff = throttle(this.computeDiff, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
|
||||
this.decorate = this.decorate.bind(this);
|
||||
|
||||
this.dirtyDiffWorker.addEventListener('message', this.decorate);
|
||||
}
|
||||
|
||||
attachModel(model) {
|
||||
if (this.models.has(model.url)) return;
|
||||
|
||||
model.onChange(() => this.throttledComputeDiff(model));
|
||||
model.onDispose(() => {
|
||||
this.decorationsController.removeDecorations(model);
|
||||
this.models.delete(model.url);
|
||||
});
|
||||
|
||||
this.models.set(model.url, model);
|
||||
}
|
||||
|
||||
computeDiff(model) {
|
||||
const originalModel = model.getOriginalModel();
|
||||
const newModel = model.getModel();
|
||||
|
||||
if (originalModel.isDisposed() || newModel.isDisposed()) return;
|
||||
|
||||
this.dirtyDiffWorker.postMessage({
|
||||
path: model.path,
|
||||
originalContent: originalModel.getValue(),
|
||||
newContent: newModel.getValue(),
|
||||
});
|
||||
}
|
||||
|
||||
reDecorate(model) {
|
||||
if (this.decorationsController.hasDecorations(model)) {
|
||||
this.decorationsController.decorate(model);
|
||||
} else {
|
||||
this.computeDiff(model);
|
||||
}
|
||||
}
|
||||
|
||||
decorate({ data }) {
|
||||
const decorations = data.changes.map((change) => getDecorator(change));
|
||||
const model = this.modelManager.getModel(data.path);
|
||||
this.decorationsController.addDecorations(model, 'dirtyDiff', decorations);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.disposable.dispose();
|
||||
this.models.clear();
|
||||
|
||||
this.dirtyDiffWorker.removeEventListener('message', this.decorate);
|
||||
this.dirtyDiffWorker.terminate();
|
||||
}
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
import { diffLines } from 'diff';
|
||||
import { defaultDiffOptions } from '../editor_options';
|
||||
|
||||
export const computeDiff = (originalContent, newContent) => {
|
||||
// prevent EOL changes from highlighting the entire file
|
||||
const changes = diffLines(
|
||||
originalContent.replace(/\r\n/g, '\n'),
|
||||
newContent.replace(/\r\n/g, '\n'),
|
||||
defaultDiffOptions,
|
||||
);
|
||||
|
||||
let lineNumber = 1;
|
||||
return changes.reduce((acc, change) => {
|
||||
const findOnLine = acc.find((c) => c.lineNumber === lineNumber);
|
||||
|
||||
if (findOnLine) {
|
||||
Object.assign(findOnLine, change, {
|
||||
modified: true,
|
||||
endLineNumber: lineNumber + change.count - 1,
|
||||
});
|
||||
} else if ('added' in change || 'removed' in change) {
|
||||
acc.push({
|
||||
...change,
|
||||
lineNumber,
|
||||
modified: undefined,
|
||||
endLineNumber: lineNumber + change.count - 1,
|
||||
});
|
||||
}
|
||||
|
||||
if (!change.removed) {
|
||||
lineNumber += change.count;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
};
|
@ -1,12 +0,0 @@
|
||||
import { computeDiff } from './diff';
|
||||
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
self.addEventListener('message', (e) => {
|
||||
const { data } = e;
|
||||
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
self.postMessage({
|
||||
path: data.path,
|
||||
changes: computeDiff(data.originalContent, data.newContent),
|
||||
});
|
||||
});
|
@ -1,241 +0,0 @@
|
||||
import { debounce } from 'lodash';
|
||||
import { editor as monacoEditor, KeyCode, KeyMod, Range } from 'monaco-editor';
|
||||
import { clearDomElement } from '~/editor/utils';
|
||||
import { registerLanguages } from '../utils';
|
||||
import Disposable from './common/disposable';
|
||||
import ModelManager from './common/model_manager';
|
||||
import DecorationsController from './decorations/controller';
|
||||
import DirtyDiffController from './diff/controller';
|
||||
import { editorOptions, defaultEditorOptions, defaultDiffEditorOptions } from './editor_options';
|
||||
import keymap from './keymap.json';
|
||||
import languages from './languages';
|
||||
import { themes } from './themes';
|
||||
|
||||
function setupThemes() {
|
||||
themes.forEach((theme) => {
|
||||
monacoEditor.defineTheme(theme.name, theme.data);
|
||||
});
|
||||
}
|
||||
|
||||
export default class Editor {
|
||||
static create(...args) {
|
||||
if (!this.editorInstance) {
|
||||
this.editorInstance = new Editor(...args);
|
||||
}
|
||||
return this.editorInstance;
|
||||
}
|
||||
|
||||
constructor(store, options = {}) {
|
||||
this.currentModel = null;
|
||||
this.instance = null;
|
||||
this.dirtyDiffController = null;
|
||||
this.disposable = new Disposable();
|
||||
this.modelManager = new ModelManager();
|
||||
this.decorationsController = new DecorationsController(this);
|
||||
this.options = {
|
||||
...defaultEditorOptions,
|
||||
...options,
|
||||
};
|
||||
this.diffOptions = {
|
||||
...defaultDiffEditorOptions,
|
||||
...options,
|
||||
};
|
||||
this.store = store;
|
||||
|
||||
setupThemes();
|
||||
registerLanguages(...languages);
|
||||
|
||||
this.debouncedUpdate = debounce(() => {
|
||||
this.updateDimensions();
|
||||
}, 200);
|
||||
}
|
||||
|
||||
createInstance(domElement) {
|
||||
if (!this.instance) {
|
||||
clearDomElement(domElement);
|
||||
|
||||
this.disposable.add(
|
||||
(this.instance = monacoEditor.create(domElement, {
|
||||
...this.options,
|
||||
})),
|
||||
(this.dirtyDiffController = new DirtyDiffController(
|
||||
this.modelManager,
|
||||
this.decorationsController,
|
||||
)),
|
||||
);
|
||||
|
||||
this.addCommands();
|
||||
|
||||
window.addEventListener('resize', this.debouncedUpdate, false);
|
||||
}
|
||||
}
|
||||
|
||||
createDiffInstance(domElement) {
|
||||
if (!this.instance) {
|
||||
clearDomElement(domElement);
|
||||
|
||||
this.disposable.add(
|
||||
(this.instance = monacoEditor.createDiffEditor(domElement, {
|
||||
...this.diffOptions,
|
||||
renderSideBySide: Editor.renderSideBySide(domElement),
|
||||
})),
|
||||
);
|
||||
|
||||
this.addCommands();
|
||||
|
||||
window.addEventListener('resize', this.debouncedUpdate, false);
|
||||
}
|
||||
}
|
||||
|
||||
createModel(file, head = null) {
|
||||
return this.modelManager.addModel(file, head);
|
||||
}
|
||||
|
||||
attachModel(model) {
|
||||
if (this.isDiffEditorType) {
|
||||
this.instance.setModel({
|
||||
original: model.getOriginalModel(),
|
||||
modified: model.getModel(),
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.instance.setModel(model.getModel());
|
||||
if (this.dirtyDiffController) this.dirtyDiffController.attachModel(model);
|
||||
|
||||
this.currentModel = model;
|
||||
|
||||
this.instance.updateOptions(
|
||||
editorOptions.reduce((acc, obj) => {
|
||||
Object.keys(obj).forEach((key) => {
|
||||
Object.assign(acc, {
|
||||
[key]: obj[key](model),
|
||||
});
|
||||
});
|
||||
return acc;
|
||||
}, {}),
|
||||
);
|
||||
|
||||
if (this.dirtyDiffController) this.dirtyDiffController.reDecorate(model);
|
||||
}
|
||||
|
||||
attachMergeRequestModel(model) {
|
||||
this.instance.setModel({
|
||||
original: model.getBaseModel(),
|
||||
modified: model.getModel(),
|
||||
});
|
||||
|
||||
monacoEditor.createDiffNavigator(this.instance, {
|
||||
alwaysRevealFirst: true,
|
||||
});
|
||||
}
|
||||
|
||||
clearEditor() {
|
||||
if (this.instance) {
|
||||
this.instance.setModel(null);
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
window.removeEventListener('resize', this.debouncedUpdate);
|
||||
|
||||
// catch any potential errors with disposing the error
|
||||
// this is mainly for tests caused by elements not existing
|
||||
try {
|
||||
this.disposable.dispose();
|
||||
|
||||
this.instance = null;
|
||||
} catch (e) {
|
||||
this.instance = null;
|
||||
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateDimensions() {
|
||||
if (this.instance) {
|
||||
this.instance.layout();
|
||||
this.updateDiffView();
|
||||
}
|
||||
}
|
||||
|
||||
setPosition({ lineNumber, column }) {
|
||||
this.instance.revealPositionInCenter({
|
||||
lineNumber,
|
||||
column,
|
||||
});
|
||||
this.instance.setPosition({
|
||||
lineNumber,
|
||||
column,
|
||||
});
|
||||
}
|
||||
|
||||
onPositionChange(cb) {
|
||||
if (!this.instance.onDidChangeCursorPosition) return;
|
||||
|
||||
this.disposable.add(this.instance.onDidChangeCursorPosition((e) => cb(this.instance, e)));
|
||||
}
|
||||
|
||||
updateDiffView() {
|
||||
if (!this.isDiffEditorType) return;
|
||||
|
||||
this.instance.updateOptions({
|
||||
renderSideBySide: Editor.renderSideBySide(this.instance.getDomNode()),
|
||||
});
|
||||
}
|
||||
|
||||
replaceSelectedText(text) {
|
||||
let selection = this.instance.getSelection();
|
||||
const range = new Range(
|
||||
selection.startLineNumber,
|
||||
selection.startColumn,
|
||||
selection.endLineNumber,
|
||||
selection.endColumn,
|
||||
);
|
||||
|
||||
this.instance.executeEdits('', [{ range, text }]);
|
||||
|
||||
selection = this.instance.getSelection();
|
||||
this.instance.setPosition({ lineNumber: selection.endLineNumber, column: selection.endColumn });
|
||||
}
|
||||
|
||||
get isDiffEditorType() {
|
||||
return this.instance.getEditorType() === 'vs.editor.IDiffEditor';
|
||||
}
|
||||
|
||||
static renderSideBySide(domElement) {
|
||||
return domElement.offsetWidth >= 700;
|
||||
}
|
||||
|
||||
addCommands() {
|
||||
const { store } = this;
|
||||
const getKeyCode = (key) => {
|
||||
const monacoKeyMod = key.indexOf('KEY_') === 0;
|
||||
|
||||
return monacoKeyMod ? KeyCode[key] : KeyMod[key];
|
||||
};
|
||||
|
||||
keymap.forEach((command) => {
|
||||
const keybindings = command.bindings.map((binding) => {
|
||||
const keys = binding.split('+');
|
||||
|
||||
// eslint-disable-next-line no-bitwise
|
||||
return keys.length > 1 ? getKeyCode(keys[0]) | getKeyCode(keys[1]) : getKeyCode(keys[0]);
|
||||
});
|
||||
|
||||
this.instance.addAction({
|
||||
id: command.id,
|
||||
label: command.label,
|
||||
keybindings,
|
||||
run() {
|
||||
store.dispatch(command.action.name, command.action.params);
|
||||
return null;
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
import { parseString } from 'editorconfig/src/lib/ini';
|
||||
import minimatch from 'minimatch';
|
||||
import { getPathParents } from '../../utils';
|
||||
|
||||
const dirname = (path) => path.replace(/\.editorconfig$/, '');
|
||||
|
||||
function isRootConfig(config) {
|
||||
return config.some(([pattern, rules]) => !pattern && rules?.root === 'true');
|
||||
}
|
||||
|
||||
function getRulesForSection(path, [pattern, rules]) {
|
||||
if (!pattern) {
|
||||
return {};
|
||||
}
|
||||
if (minimatch(path, pattern, { matchBase: true })) {
|
||||
return rules;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
function getRulesWithConfigs(filePath, configFiles = [], rules = {}) {
|
||||
if (!configFiles.length) return rules;
|
||||
|
||||
const [{ content, path: configPath }, ...nextConfigs] = configFiles;
|
||||
const configDir = dirname(configPath);
|
||||
|
||||
if (!filePath.startsWith(configDir)) return rules;
|
||||
|
||||
const parsed = parseString(content);
|
||||
const isRoot = isRootConfig(parsed);
|
||||
const relativeFilePath = filePath.slice(configDir.length);
|
||||
|
||||
const sectionRules = parsed.reduce(
|
||||
(acc, section) => Object.assign(acc, getRulesForSection(relativeFilePath, section)),
|
||||
{},
|
||||
);
|
||||
|
||||
// prefer existing rules by overwriting to section rules
|
||||
const result = Object.assign(sectionRules, rules);
|
||||
|
||||
return isRoot ? result : getRulesWithConfigs(filePath, nextConfigs, result);
|
||||
}
|
||||
|
||||
export function getRulesWithTraversal(filePath, getFileContent) {
|
||||
const editorconfigPaths = [
|
||||
...getPathParents(filePath).map((x) => `${x}/.editorconfig`),
|
||||
'.editorconfig',
|
||||
];
|
||||
|
||||
return Promise.all(
|
||||
editorconfigPaths.map((path) => getFileContent(path).then((content) => ({ path, content }))),
|
||||
).then((results) =>
|
||||
getRulesWithConfigs(
|
||||
filePath,
|
||||
results.filter((x) => x.content),
|
||||
),
|
||||
);
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
import { isBoolean, isNumber } from 'lodash';
|
||||
|
||||
const map = (key, validValues) => (value) =>
|
||||
value in validValues ? { [key]: validValues[value] } : {};
|
||||
|
||||
const bool = (key) => (value) => (isBoolean(value) ? { [key]: value } : {});
|
||||
|
||||
const int = (key, isValid) => (value) =>
|
||||
isNumber(value) && isValid(value) ? { [key]: Math.trunc(value) } : {};
|
||||
|
||||
const rulesMapper = {
|
||||
indent_style: map('insertSpaces', { tab: false, space: true }),
|
||||
indent_size: int('tabSize', (n) => n > 0),
|
||||
tab_width: int('tabSize', (n) => n > 0),
|
||||
trim_trailing_whitespace: bool('trimTrailingWhitespace'),
|
||||
end_of_line: map('endOfLine', { crlf: 1, lf: 0 }),
|
||||
insert_final_newline: bool('insertFinalNewline'),
|
||||
};
|
||||
|
||||
const parseValue = (x) => {
|
||||
let value = typeof x === 'string' ? x.toLowerCase() : x;
|
||||
if (/^[0-9.-]+$/.test(value)) value = Number(value);
|
||||
if (value === 'true') value = true;
|
||||
if (value === 'false') value = false;
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
export default function mapRulesToMonaco(rules) {
|
||||
return Object.entries(rules).reduce((obj, [key, value]) => {
|
||||
return Object.assign(obj, rulesMapper[key]?.(parseValue(value)) || {});
|
||||
}, {});
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
import { escape } from 'lodash';
|
||||
import { __ } from '~/locale';
|
||||
import { COMMIT_TO_NEW_BRANCH } from '../stores/modules/commit/constants';
|
||||
|
||||
const CODEOWNERS_REGEX = /Push.*protected branches.*CODEOWNERS/;
|
||||
const BRANCH_CHANGED_REGEX = /changed.*since.*start.*edit/;
|
||||
const BRANCH_ALREADY_EXISTS = /branch.*already.*exists/;
|
||||
|
||||
const createNewBranchAndCommit = (store) =>
|
||||
store
|
||||
.dispatch('commit/updateCommitAction', COMMIT_TO_NEW_BRANCH)
|
||||
.then(() => store.dispatch('commit/commitChanges'));
|
||||
|
||||
export const createUnexpectedCommitError = (message) => ({
|
||||
title: __('Unexpected error'),
|
||||
messageHTML: escape(message) || __('Could not commit. An unexpected error occurred.'),
|
||||
});
|
||||
|
||||
export const createCodeownersCommitError = (message) => ({
|
||||
title: __('CODEOWNERS rule violation'),
|
||||
messageHTML: escape(message),
|
||||
primaryAction: {
|
||||
text: __('Create new branch'),
|
||||
callback: createNewBranchAndCommit,
|
||||
},
|
||||
});
|
||||
|
||||
export const createBranchChangedCommitError = (message) => ({
|
||||
title: __('Branch changed'),
|
||||
messageHTML: `${escape(message)}<br/><br/>${__('Would you like to create a new branch?')}`,
|
||||
primaryAction: {
|
||||
text: __('Create new branch'),
|
||||
callback: createNewBranchAndCommit,
|
||||
},
|
||||
});
|
||||
|
||||
export const branchAlreadyExistsCommitError = (message) => ({
|
||||
title: __('Branch already exists'),
|
||||
messageHTML: `${escape(message)}<br/><br/>${__(
|
||||
'Would you like to try auto-generating a branch name?',
|
||||
)}`,
|
||||
primaryAction: {
|
||||
text: __('Create new branch'),
|
||||
callback: (store) =>
|
||||
store.dispatch('commit/addSuffixToBranchName').then(() => createNewBranchAndCommit(store)),
|
||||
},
|
||||
});
|
||||
|
||||
export const parseCommitError = (e) => {
|
||||
const { message } = e?.response?.data || {};
|
||||
|
||||
if (!message) {
|
||||
return createUnexpectedCommitError();
|
||||
}
|
||||
|
||||
if (CODEOWNERS_REGEX.test(message)) {
|
||||
return createCodeownersCommitError(message);
|
||||
}
|
||||
if (BRANCH_CHANGED_REGEX.test(message)) {
|
||||
return createBranchChangedCommitError(message);
|
||||
}
|
||||
if (BRANCH_ALREADY_EXISTS.test(message)) {
|
||||
return branchAlreadyExistsCommitError(message);
|
||||
}
|
||||
|
||||
return createUnexpectedCommitError(message);
|
||||
};
|
@ -1,110 +0,0 @@
|
||||
import { decorateData, sortTree } from '../stores/utils';
|
||||
|
||||
export const splitParent = (path) => {
|
||||
const idx = path.lastIndexOf('/');
|
||||
|
||||
return {
|
||||
parent: idx >= 0 ? path.substring(0, idx) : null,
|
||||
name: idx >= 0 ? path.substring(idx + 1) : path,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Create file objects from a list of file paths.
|
||||
*
|
||||
* @param {Array} options.data Array of blob paths to parse and create a file tree from.
|
||||
* @param {Boolean} options.tempFile Web IDE flag for whether this is a "new" file or not.
|
||||
* @param {String} options.content Content to initialize the new blob with.
|
||||
* @param {String} options.rawPath Raw path used for the new blob.
|
||||
* @param {Object} options.blobData Extra values to initialize each blob with.
|
||||
*/
|
||||
export const decorateFiles = ({
|
||||
data,
|
||||
tempFile = false,
|
||||
content = '',
|
||||
rawPath = '',
|
||||
blobData = {},
|
||||
}) => {
|
||||
const treeList = [];
|
||||
const entries = {};
|
||||
|
||||
// These mutable variable references end up being exported and used by `createTempEntry`
|
||||
let file;
|
||||
let parentPath;
|
||||
|
||||
const insertParent = (path) => {
|
||||
if (!path) {
|
||||
return null;
|
||||
}
|
||||
if (entries[path]) {
|
||||
return entries[path];
|
||||
}
|
||||
|
||||
const { parent, name } = splitParent(path);
|
||||
const parentFolder = parent && insertParent(parent);
|
||||
parentPath = parentFolder && parentFolder.path;
|
||||
|
||||
const tree = decorateData({
|
||||
id: path,
|
||||
name,
|
||||
path,
|
||||
type: 'tree',
|
||||
tempFile,
|
||||
changed: tempFile,
|
||||
opened: tempFile,
|
||||
parentPath,
|
||||
});
|
||||
|
||||
Object.assign(entries, {
|
||||
[path]: tree,
|
||||
});
|
||||
|
||||
if (parentFolder) {
|
||||
parentFolder.tree.push(tree);
|
||||
} else {
|
||||
treeList.push(tree);
|
||||
}
|
||||
|
||||
return tree;
|
||||
};
|
||||
|
||||
data.forEach((path) => {
|
||||
const { parent, name } = splitParent(path);
|
||||
|
||||
const fileFolder = parent && insertParent(parent);
|
||||
|
||||
if (name) {
|
||||
parentPath = fileFolder && fileFolder.path;
|
||||
|
||||
file = decorateData({
|
||||
id: path,
|
||||
name,
|
||||
path,
|
||||
type: 'blob',
|
||||
tempFile,
|
||||
changed: tempFile,
|
||||
content,
|
||||
rawPath,
|
||||
parentPath,
|
||||
...blobData,
|
||||
});
|
||||
|
||||
Object.assign(entries, {
|
||||
[path]: file,
|
||||
});
|
||||
|
||||
if (fileFolder) {
|
||||
fileFolder.tree.push(file);
|
||||
} else {
|
||||
treeList.push(file);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
entries,
|
||||
treeList: sortTree(treeList),
|
||||
file,
|
||||
parentPath,
|
||||
};
|
||||
};
|
@ -1,19 +0,0 @@
|
||||
[
|
||||
{
|
||||
"id": "file-finder",
|
||||
"label": "File finder",
|
||||
"bindings": ["CtrlCmd+KEY_P"],
|
||||
"action": {
|
||||
"name": "toggleFileFinder",
|
||||
"params": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "save-files",
|
||||
"label": "Save files",
|
||||
"bindings": ["CtrlCmd+KEY_S"],
|
||||
"action": {
|
||||
"name": "triggerFilesChange"
|
||||
}
|
||||
}
|
||||
]
|
@ -1,156 +0,0 @@
|
||||
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
|
||||
import { getWebSocketUrl, mergeUrlParams } from '~/lib/utils/url_utility';
|
||||
import { __ } from '~/locale';
|
||||
import createDiff from './create_diff';
|
||||
|
||||
export const SERVICE_NAME = 'webide-file-sync';
|
||||
export const PROTOCOL = 'webfilesync.gitlab.com';
|
||||
export const MSG_CONNECTION_ERROR = __('Could not connect to Web IDE file mirror service.');
|
||||
|
||||
// Before actually connecting to the service, we must delay a bit
|
||||
// so that the service has sufficiently started.
|
||||
|
||||
const noop = () => {};
|
||||
export const SERVICE_DELAY = 8000;
|
||||
|
||||
const cancellableWait = (time) => {
|
||||
let timeoutId = 0;
|
||||
|
||||
const cancel = () => clearTimeout(timeoutId);
|
||||
|
||||
const promise = new Promise((resolve) => {
|
||||
timeoutId = setTimeout(resolve, time);
|
||||
});
|
||||
|
||||
return [promise, cancel];
|
||||
};
|
||||
|
||||
const isErrorResponse = (error) => error && error.code !== 0;
|
||||
|
||||
const isErrorPayload = (payload) => payload && payload.status_code !== HTTP_STATUS_OK;
|
||||
|
||||
const getErrorFromResponse = (data) => {
|
||||
if (isErrorResponse(data.error)) {
|
||||
return { message: data.error.Message };
|
||||
}
|
||||
if (isErrorPayload(data.payload)) {
|
||||
return { message: data.payload.error_message };
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const getFullPath = (path) => mergeUrlParams({ service: SERVICE_NAME }, getWebSocketUrl(path));
|
||||
|
||||
const createWebSocket = (fullPath) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const socket = new WebSocket(fullPath, [PROTOCOL]);
|
||||
const resetCallbacks = () => {
|
||||
socket.onopen = null;
|
||||
socket.onerror = null;
|
||||
};
|
||||
|
||||
socket.onopen = () => {
|
||||
resetCallbacks();
|
||||
resolve(socket);
|
||||
};
|
||||
|
||||
socket.onerror = () => {
|
||||
resetCallbacks();
|
||||
reject(new Error(MSG_CONNECTION_ERROR));
|
||||
};
|
||||
});
|
||||
|
||||
export const canConnect = ({ services = [] }) => services.some((name) => name === SERVICE_NAME);
|
||||
|
||||
export const createMirror = () => {
|
||||
let socket = null;
|
||||
let cancelHandler = noop;
|
||||
let nextMessageHandler = noop;
|
||||
|
||||
const cancelConnect = () => {
|
||||
cancelHandler();
|
||||
cancelHandler = noop;
|
||||
};
|
||||
|
||||
const onCancelConnect = (fn) => {
|
||||
cancelHandler = fn;
|
||||
};
|
||||
|
||||
const receiveMessage = (ev) => {
|
||||
const handle = nextMessageHandler;
|
||||
nextMessageHandler = noop;
|
||||
handle(JSON.parse(ev.data));
|
||||
};
|
||||
|
||||
const onNextMessage = (fn) => {
|
||||
nextMessageHandler = fn;
|
||||
};
|
||||
|
||||
const waitForNextMessage = () =>
|
||||
new Promise((resolve, reject) => {
|
||||
onNextMessage((data) => {
|
||||
const err = getErrorFromResponse(data);
|
||||
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const uploadDiff = ({ toDelete, patch }) => {
|
||||
if (!socket) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const response = waitForNextMessage();
|
||||
|
||||
const msg = {
|
||||
code: 'EVENT',
|
||||
namespace: '/files',
|
||||
event: 'PATCH',
|
||||
payload: { diff: patch, delete_files: toDelete },
|
||||
};
|
||||
|
||||
socket.send(JSON.stringify(msg));
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
return {
|
||||
upload(state) {
|
||||
return uploadDiff(createDiff(state));
|
||||
},
|
||||
connect(path) {
|
||||
if (socket) {
|
||||
this.disconnect();
|
||||
}
|
||||
|
||||
const fullPath = getFullPath(path);
|
||||
const [wait, cancelWait] = cancellableWait(SERVICE_DELAY);
|
||||
|
||||
onCancelConnect(cancelWait);
|
||||
|
||||
return wait
|
||||
.then(() => createWebSocket(fullPath))
|
||||
.then((newSocket) => {
|
||||
socket = newSocket;
|
||||
socket.onmessage = receiveMessage;
|
||||
});
|
||||
},
|
||||
disconnect() {
|
||||
cancelConnect();
|
||||
|
||||
if (!socket) {
|
||||
return;
|
||||
}
|
||||
|
||||
socket.close();
|
||||
socket = null;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default createMirror();
|
@ -1,25 +0,0 @@
|
||||
import { s__ } from '~/locale';
|
||||
|
||||
export const MSG_CANNOT_PUSH_CODE_SHOULD_FORK = s__(
|
||||
'WebIDE|You can’t edit files directly in this project. Fork this project and submit a merge request with your changes.',
|
||||
);
|
||||
|
||||
export const MSG_CANNOT_PUSH_CODE_GO_TO_FORK = s__(
|
||||
'WebIDE|You can’t edit files directly in this project. Go to your fork and submit a merge request with your changes.',
|
||||
);
|
||||
|
||||
export const MSG_CANNOT_PUSH_CODE = s__(
|
||||
'WebIDE|You need permission to edit files directly in this project.',
|
||||
);
|
||||
|
||||
export const MSG_CANNOT_PUSH_UNSIGNED = s__(
|
||||
'WebIDE|This project does not accept unsigned commits. You can’t commit changes through the Web IDE.',
|
||||
);
|
||||
|
||||
export const MSG_CANNOT_PUSH_UNSIGNED_SHORT = s__(
|
||||
'WebIDE|This project does not accept unsigned commits.',
|
||||
);
|
||||
|
||||
export const MSG_FORK = s__('WebIDE|Fork project');
|
||||
|
||||
export const MSG_GO_TO_FORK = s__('WebIDE|Go to fork');
|
@ -1,7 +0,0 @@
|
||||
#import "~/ide/queries/ide_project.fragment.graphql"
|
||||
|
||||
query getIdeProject($projectPath: ID!) {
|
||||
project(fullPath: $projectPath) {
|
||||
...IdeProject
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
fragment IdeProject on Project {
|
||||
id
|
||||
userPermissions {
|
||||
createMergeRequestIn
|
||||
readMergeRequest
|
||||
pushCode
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
import { memoize } from 'lodash';
|
||||
import createGqClient, { fetchPolicies } from '~/lib/graphql';
|
||||
|
||||
/**
|
||||
* Returns a memoized client
|
||||
*
|
||||
* We defer creating the client so that importing this module does not cause any side-effects.
|
||||
* Creating the client immediately caused issues with miragejs where the gql client uses the
|
||||
* real fetch() instead of the shimmed one.
|
||||
*/
|
||||
const getClient = memoize(() =>
|
||||
createGqClient(
|
||||
{},
|
||||
{
|
||||
fetchPolicy: fetchPolicies.NO_CACHE,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
export const query = (...args) => getClient().query(...args);
|
||||
export const mutate = (...args) => getClient().mutate(...args);
|
@ -1,90 +1,7 @@
|
||||
import Api from '~/api';
|
||||
import getIdeProject from 'ee_else_ce/ide/queries/get_ide_project.query.graphql';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility';
|
||||
import { query } from './gql';
|
||||
|
||||
export default {
|
||||
getFileData(endpoint) {
|
||||
return axios.get(endpoint, {
|
||||
params: { format: 'json', viewer: 'none' },
|
||||
});
|
||||
},
|
||||
getRawFileData(file) {
|
||||
if (file.tempFile && !file.prevPath) {
|
||||
return Promise.resolve(file.content);
|
||||
}
|
||||
|
||||
if (file.raw || !file.rawPath) {
|
||||
return Promise.resolve(file.raw);
|
||||
}
|
||||
|
||||
const options = file.binary ? { responseType: 'arraybuffer' } : {};
|
||||
|
||||
return axios
|
||||
.get(file.rawPath, {
|
||||
transformResponse: [(f) => f],
|
||||
...options,
|
||||
})
|
||||
.then(({ data }) => data);
|
||||
},
|
||||
getBaseRawFileData(file, projectId, ref) {
|
||||
if (file.tempFile || file.baseRaw) return Promise.resolve(file.baseRaw);
|
||||
|
||||
// if files are renamed, their base path has changed
|
||||
const filePath =
|
||||
file.mrChange && file.mrChange.renamed_file ? file.mrChange.old_path : file.path;
|
||||
|
||||
return axios
|
||||
.get(
|
||||
joinPaths(
|
||||
gon.relative_url_root || '/',
|
||||
projectId,
|
||||
'-',
|
||||
'raw',
|
||||
ref,
|
||||
escapeFileUrl(filePath),
|
||||
),
|
||||
{
|
||||
transformResponse: [(f) => f],
|
||||
},
|
||||
)
|
||||
.then(({ data }) => data);
|
||||
},
|
||||
getProjectMergeRequests(projectId, params = {}) {
|
||||
return Api.projectMergeRequests(projectId, params);
|
||||
},
|
||||
getProjectMergeRequestData(projectId, mergeRequestId, params = {}) {
|
||||
return Api.projectMergeRequest(projectId, mergeRequestId, params);
|
||||
},
|
||||
getProjectMergeRequestChanges(projectId, mergeRequestId) {
|
||||
return Api.projectMergeRequestChanges(projectId, mergeRequestId);
|
||||
},
|
||||
getProjectMergeRequestVersions(projectId, mergeRequestId) {
|
||||
return Api.projectMergeRequestVersions(projectId, mergeRequestId);
|
||||
},
|
||||
getBranchData(projectId, currentBranchId) {
|
||||
return Api.branchSingle(projectId, currentBranchId);
|
||||
},
|
||||
commit(projectId, payload) {
|
||||
return Api.commitMultiple(projectId, payload);
|
||||
},
|
||||
getFiles(projectPath, ref) {
|
||||
const url = `${gon.relative_url_root}/${projectPath}/-/files/${ref}`;
|
||||
return axios.get(url, { params: { format: 'json' } });
|
||||
},
|
||||
lastCommitPipelines({ getters }) {
|
||||
const commitSha = getters.lastCommit.id;
|
||||
return Api.commitPipelines(getters.currentProject.path_with_namespace, commitSha);
|
||||
},
|
||||
getProjectPermissionsData(projectPath) {
|
||||
return query({
|
||||
query: getIdeProject,
|
||||
variables: { projectPath },
|
||||
}).then(({ data }) => ({
|
||||
...data.project,
|
||||
id: getIdFromGraphQLId(data.project.id),
|
||||
}));
|
||||
},
|
||||
};
|
||||
|
@ -1,17 +0,0 @@
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { joinPaths } from '~/lib/utils/url_utility';
|
||||
|
||||
export const baseUrl = (projectPath) =>
|
||||
joinPaths(gon.relative_url_root || '', `/${projectPath}/ide_terminals`);
|
||||
|
||||
export const checkConfig = (projectPath, branch) =>
|
||||
axios.post(`${baseUrl(projectPath)}/check_config`, {
|
||||
branch,
|
||||
format: 'json',
|
||||
});
|
||||
|
||||
export const create = (projectPath, branch) =>
|
||||
axios.post(baseUrl(projectPath), {
|
||||
branch,
|
||||
format: 'json',
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user