packages/core/src/codecatalyst/vue/create/source.vue (309 lines of code) (raw):
<template>
<div class="modes mb-16">
<label class="mode-container" :data-disabled="model.type !== 'linked'">
<input class="radio" type="radio" name="mode" id="from-code-catalyst" v-model="model.type" value="linked" />
<span class="ml-8 option-label" style="padding: 0px">Use an existing CodeCatalyst repository</span>
</label>
<label class="mode-container" :data-disabled="model.type !== 'none'">
<input class="radio" type="radio" name="mode" id="from-none" v-model="model.type" value="none" />
<span class="ml-8 option-label" style="padding: 0px">Create an empty Dev Environment</span>
</label>
</div>
<div class="source-pickers" v-if="model.type === 'linked'">
<div class="modes flex-sizing mt-16">
<span class="flex-sizing mt-8">
<label class="option-label soft">Space</label>
<button class="project-button" @click="quickPickSpace()">
{{ selectedSpaceName }}
<span class="icon icon-lg icon-vscode-edit edit-icon"></span>
</button>
</span>
<span class="flex-sizing mt-8">
<label class="option-label soft">Project</label>
<button class="project-button" @click="quickPickProject()" :disabled="!isSpaceSelected">
{{ selectedProjectName }}
<span class="icon icon-lg icon-vscode-edit edit-icon"></span>
</button>
</span>
</div>
<div class="modes flex-sizing mt-16">
<!-- Existing branch -->
<span class="flex-sizing">
<label class="options-label soft mb-8" style="display: block" for="branch-picker">Branch</label>
<select
id="branch-picker"
class="picker"
:disabled="!model.selectedProject"
v-model="model.selectedBranch"
@input="update"
>
<option disabled :value="undefined">{{ branchPlaceholder }}</option>
<option v-for="branch in availableBranches" :key="branch.id" :value="branch">
{{ branchName(branch) }}
</option>
</select>
</span>
<!-- New Branch -->
<span class="flex-sizing">
<label class="options-label soft mb-8" style="display: block" for="branch-input"
>Create Branch - Optional for CodeCatalyst Repos</label
>
<input
id="branch-input"
type="text"
:placeholder="newBranchNamePlaceholder"
:disabled="!newBranchNameAllowed"
v-model="model.newBranch"
@input="update"
/>
<div class="input-validation" v-if="branchError">{{ branchError }}</div>
</span>
</div>
</div>
<div class="source-pickers" v-if="model.type === 'none'">
<div class="modes flex-sizing mt-16">
<span class="flex-sizing mt-8">
<label class="option-label soft">Space</label>
<button class="project-button" @click="quickPickSpace()">
{{ selectedSpaceName }}
<span class="icon icon-lg icon-vscode-edit edit-icon"></span>
</button>
</span>
<span class="flex-sizing mt-8">
<label class="option-label soft">Project</label>
<button class="project-button" @click="quickPickProject()" :disabled="!isSpaceSelected">
{{ selectedProjectName }}
<span class="icon icon-lg icon-vscode-edit edit-icon"></span>
</button>
</span>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { CodeCatalystBranch, CodeCatalystProject } from '../../../shared/clients/codecatalystClient'
import { WebviewClientFactory } from '../../../webviews/client'
import { createClass, createType } from '../../../webviews/util'
import { CodeCatalystCreateWebview, SourceResponse } from './backend'
const client = WebviewClientFactory.create<CodeCatalystCreateWebview>()
type SourceModel = Partial<SourceResponse & { branchError: string }>
export function isValidSource(source: SourceModel): source is SourceResponse {
if (source.type === 'linked') {
return !!source.selectedProject && !!source.selectedBranch && !source.branchError
} else if (source.type === 'none') {
return !!source.selectedProject
}
return false
}
export const VueModel = createClass<SourceModel>({ type: 'linked' })
export default defineComponent({
name: 'source-code',
props: {
modelValue: {
type: createType(VueModel),
default: new VueModel(),
},
},
data() {
return {
projects: [] as CodeCatalystProject[],
branches: {} as Record<string, CodeCatalystBranch[] | undefined>,
loadingProjects: false,
loadingBranches: {} as Record<string, boolean | undefined>,
newBranchNameAllowed: false,
newBranchNamePlaceholder: 'Select branch first...',
}
},
async created() {
this.loadingProjects = true
},
watch: {
async 'model.selectedProject'(project?: CodeCatalystProject) {
this.useFirstBranch()
if (project && !this.branches[project.name]) {
this.loadingBranches[project.name] = true
this.branches[project.name] ??= await client.getBranches(project).finally(() => {
this.loadingBranches[project.name] = false
})
this.useFirstBranch()
}
},
async 'model.selectedBranch'(branch?: CodeCatalystBranch) {
if (this.model.type !== 'linked' || branch === undefined) {
this.newBranchNameAllowed = false
this.newBranchNamePlaceholder = ''
return
}
// Disable user input for new branch name while calculating
this.newBranchNameAllowed = false
this.newBranchNamePlaceholder = 'Loading...'
// Clear the existing new branch value so user does not see it
const previousNewBranch = this.model.newBranch
this.$emit('update:modelValue', { ...this.model, newBranch: undefined })
// Only want to allow users to set a branch name if first party repo
const isThirdPartyRepo = await client.isThirdPartyRepo({
spaceName: branch.org.name,
projectName: branch.project.name,
sourceRepositoryName: branch.repo.name,
})
if (isThirdPartyRepo) {
this.newBranchNamePlaceholder = 'Not Applicable for Linked Repo'
this.newBranchNameAllowed = false
// Clear the new branch in case one was already selected
this.$emit('update:modelValue', { ...this.model, newBranch: undefined })
} else {
// First Party
this.newBranchNamePlaceholder = 'branch-name'
this.newBranchNameAllowed = true
// Since this can have a new branch, set this back to what it previously was
this.$emit('update:modelValue', { ...this.model, newBranch: previousNewBranch })
}
},
},
computed: {
model() {
return this.modelValue
},
loading() {
if (this.model.type !== 'linked' || !this.model.selectedProject) {
return false
}
return this.loadingBranches[this.model.selectedProject.name] ?? false
},
branchPlaceholder() {
if (this.loading) {
return 'Loading...'
}
return (this.availableBranches?.length ?? 0) === 0 ? 'No branches found' : 'Select a branch'
},
availableBranches() {
if (this.model.type !== 'linked' || !this.model.selectedProject) {
return []
}
return this.branches[this.model.selectedProject.name]
},
branchError() {
if (this.model.type !== 'linked' || !this.model.newBranch) {
return
}
const branch = this.model.newBranch
if (!!branch && this.availableBranches?.find(b => b.name === `refs/heads/${branch}`) !== undefined) {
return 'Branch already exists'
}
},
isSpaceSelected() {
return !!this.model.selectedSpace
},
isProjectSelected() {
return !!this.model.selectedProject
},
selectedSpaceName() {
if (this.model.selectedSpace === undefined) {
return 'Not Selected'
}
return this.model.selectedSpace.name
},
selectedProjectName() {
if (this.model.selectedProject === undefined) {
return 'Not Selected'
}
return this.model.selectedProject.name
},
},
methods: {
update() {
this.model.branchError = this.branchError
this.$emit('update:modelValue', this.model)
},
branchName(branch: CodeCatalystBranch) {
return `${branch.repo.name} / ${branch.name.replace('refs/heads/', '')}`
},
useFirstBranch() {
if (this.model.type !== 'linked') {
return
}
Object.assign<typeof this.model, Partial<SourceModel>>(this.model, {
selectedBranch: this.availableBranches?.[0],
})
this.update()
},
async quickPickSpace() {
const space = await client.quickPickSpace()
if (space === undefined) {
return
}
this.$emit('update:modelValue', { ...this.modelValue, selectedSpace: space, selectedProject: undefined })
},
async quickPickProject() {
const selectedSpace = this.modelValue.selectedSpace
if (selectedSpace === undefined) {
return
}
const project = await client.quickPickProject(selectedSpace.name)
if (project === undefined) {
return
}
this.$emit('update:modelValue', { ...this.modelValue, selectedProject: project })
},
},
emits: {
'update:modelValue': (value: InstanceType<typeof VueModel>) => true,
},
})
</script>
<style scope>
.picker {
min-width: 300px;
width: 100%;
box-sizing: border-box;
}
.source-pickers {
margin-top: 16px;
display: flex;
flex-flow: wrap;
column-gap: 16px;
}
.modes {
display: flex;
column-gap: 16px;
}
.flex-sizing {
flex: 1;
}
.mode-container {
display: flex;
flex: 1;
border: 1px solid gray;
padding: 8px;
max-width: calc((1 / 3 * 100%) - (2 / 3 * 32px));
align-items: center;
}
.config-item {
display: inline;
margin-left: 8px;
}
.mode-container[data-disabled='false'] {
border: 1px solid var(--vscode-focusBorder);
}
body.vscode-dark .mode-container[data-disabled='true'] .config-item {
filter: brightness(0.8);
}
body.vscode-light .mode-container[data-disabled='true'] .config-item {
filter: brightness(1.2);
}
#repository-url {
min-width: 300px;
}
#branch-input {
min-width: 300px;
width: 100%;
box-sizing: border-box;
}
.project-button {
background-color: transparent;
padding-left: 0;
padding-right: 0;
font-weight: bold;
}
.edit-icon {
color: #0078d7;
}
</style>