Highlight invalid entries in manual pipeline trigger (#4153)

and move key-value editor into own component
This commit is contained in:
6543 2024-10-06 15:34:20 +02:00 committed by GitHub
parent 7ff14530d4
commit 496859c083
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 129 additions and 60 deletions

View file

@ -0,0 +1,107 @@
<template>
<div class="flex flex-col gap-2">
<div v-for="(item, index) in displayItems" :key="index" class="flex gap-4">
<TextField
:id="`${id}-key-${index}`"
:model-value="item.key"
:placeholder="keyPlaceholder"
:class="{
'bg-red-100 dark:bg-red-900':
isDuplicateKey(item.key, index) || (item.key === '' && index !== displayItems.length - 1),
}"
@update:model-value="updateItem(index, 'key', $event)"
/>
<TextField
:id="`${id}-value-${index}`"
:model-value="item.value"
:placeholder="valuePlaceholder"
@update:model-value="updateItem(index, 'value', $event)"
/>
<div class="w-10 flex-shrink-0">
<Button
v-if="index !== displayItems.length - 1"
type="button"
color="red"
class="ml-auto"
:title="deleteTitle"
@click="deleteItem(index)"
>
<Icon name="remove" />
</Button>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import Button from '~/components/atomic/Button.vue';
import Icon from '~/components/atomic/Icon.vue';
import TextField from '~/components/form/TextField.vue';
const props = defineProps<{
modelValue: Record<string, string>;
id?: string;
keyPlaceholder?: string;
valuePlaceholder?: string;
deleteTitle?: string;
}>();
const emit = defineEmits<{
(e: 'update:modelValue', value: Record<string, string>): void;
(e: 'update:isValid', value: boolean): void;
}>();
const items = ref(Object.entries(props.modelValue).map(([key, value]) => ({ key, value })));
const displayItems = computed(() => {
if (items.value.length === 0 || items.value[items.value.length - 1].key !== '') {
return [...items.value, { key: '', value: '' }];
}
return items.value;
});
function isDuplicateKey(key: string, index: number): boolean {
return items.value.some((item, i) => item.key === key && i !== index && key !== '');
}
function checkValidity() {
const isValid = items.value.every(
(item, idx) => !isDuplicateKey(item.key, idx) && (item.key !== '' || idx === items.value.length - 1),
);
emit('update:isValid', isValid);
}
function updateItem(index: number, field: 'key' | 'value', value: string) {
const newItems = [...items.value];
if (index === newItems.length) {
newItems.push({ key: '', value: '' });
}
newItems[index][field] = value;
items.value = newItems;
const newValue = Object.fromEntries(
newItems
.filter((item) => item.key !== '' && !isDuplicateKey(item.key, newItems.indexOf(item)))
.map((item) => [item.key, item.value]),
);
emit('update:modelValue', newValue);
checkValidity();
}
function deleteItem(index: number) {
items.value = items.value.filter((_, i) => i !== index);
const newValue = Object.fromEntries(
items.value
.filter((item) => item.key !== '' && !isDuplicateKey(item.key, items.value.indexOf(item)))
.map((item) => [item.key, item.value]),
);
emit('update:modelValue', newValue);
checkValidity();
}
</script>

View file

@ -7,33 +7,16 @@
</InputField> </InputField>
<InputField v-slot="{ id }" :label="$t('repo.manual_pipeline.variables.title')"> <InputField v-slot="{ id }" :label="$t('repo.manual_pipeline.variables.title')">
<span class="text-sm text-wp-text-alt-100 mb-2">{{ $t('repo.manual_pipeline.variables.desc') }}</span> <span class="text-sm text-wp-text-alt-100 mb-2">{{ $t('repo.manual_pipeline.variables.desc') }}</span>
<div class="flex flex-col gap-2"> <KeyValueEditor
<div v-for="(_, i) in payload.variables" :key="i" class="flex gap-4"> :id="id"
<TextField v-model="payload.variables"
:id="id" :key-placeholder="$t('repo.manual_pipeline.variables.name')"
v-model="payload.variables[i].name" :value-placeholder="$t('repo.manual_pipeline.variables.value')"
:placeholder="$t('repo.manual_pipeline.variables.name')" :delete-title="$t('repo.manual_pipeline.variables.delete')"
/> @update:is-valid="isVariablesValid = $event"
<TextField />
:id="id"
v-model="payload.variables[i].value"
:placeholder="$t('repo.manual_pipeline.variables.value')"
/>
<div class="w-10 flex-shrink-0">
<Button
v-if="i !== payload.variables.length - 1"
color="red"
class="ml-auto"
:title="$t('repo.manual_pipeline.variables.delete')"
@click="deleteVar(i)"
>
<Icon name="remove" />
</Button>
</div>
</div>
</div>
</InputField> </InputField>
<Button type="submit" :text="$t('repo.manual_pipeline.trigger')" /> <Button type="submit" :text="$t('repo.manual_pipeline.trigger')" :disabled="!isFormValid" />
</form> </form>
</Panel> </Panel>
<div v-else class="flex justify-center text-wp-text-100"> <div v-else class="flex justify-center text-wp-text-100">
@ -44,15 +27,15 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useNotification } from '@kyvg/vue3-notification'; import { useNotification } from '@kyvg/vue3-notification';
import type { Ref } from 'vue'; import type { Ref } from 'vue';
import { computed, onMounted, ref, inject as vueInject, watch } from 'vue'; import { computed, onMounted, ref, inject as vueInject } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import Button from '~/components/atomic/Button.vue'; import Button from '~/components/atomic/Button.vue';
import Icon from '~/components/atomic/Icon.vue'; import Icon from '~/components/atomic/Icon.vue';
import InputField from '~/components/form/InputField.vue'; import InputField from '~/components/form/InputField.vue';
import KeyValueEditor from '~/components/form/KeyValueEditor.vue';
import SelectField from '~/components/form/SelectField.vue'; import SelectField from '~/components/form/SelectField.vue';
import TextField from '~/components/form/TextField.vue';
import Panel from '~/components/layout/Panel.vue'; import Panel from '~/components/layout/Panel.vue';
import useApiClient from '~/compositions/useApiClient'; import useApiClient from '~/compositions/useApiClient';
import { inject } from '~/compositions/useInjectProvide'; import { inject } from '~/compositions/useInjectProvide';
@ -79,26 +62,22 @@ if (!repoPermissions) {
const router = useRouter(); const router = useRouter();
const branches = ref<{ text: string; value: string }[]>([]); const branches = ref<{ text: string; value: string }[]>([]);
const payload = ref<{ branch: string; variables: { name: string; value: string }[] }>({ const payload = ref<{ branch: string; variables: Record<string, string> }>({
branch: 'main', branch: 'main',
variables: [ variables: {},
{
name: '',
value: '',
},
],
}); });
const pipelineOptions = computed(() => { const isVariablesValid = ref(true);
const variables = Object.fromEntries(
payload.value.variables.filter((e) => e.name !== '').map((item) => [item.name, item.value]), const isFormValid = computed(() => {
); return payload.value.branch !== '' && isVariablesValid.value;
return {
...payload.value,
variables,
};
}); });
const pipelineOptions = computed(() => ({
...payload.value,
variables: payload.value.variables,
}));
const loading = ref(true); const loading = ref(true);
onMounted(async () => { onMounted(async () => {
if (!repoPermissions.value.push) { if (!repoPermissions.value.push) {
@ -114,23 +93,6 @@ onMounted(async () => {
loading.value = false; loading.value = false;
}); });
watch(
payload,
() => {
if (payload.value.variables[payload.value.variables.length - 1].name !== '') {
payload.value.variables.push({
name: '',
value: '',
});
}
},
{ deep: true },
);
function deleteVar(index: number) {
payload.value.variables.splice(index, 1);
}
async function triggerManualPipeline() { async function triggerManualPipeline() {
loading.value = true; loading.value = true;
const pipeline = await apiClient.createPipeline(repo.value.id, pipelineOptions.value); const pipeline = await apiClient.createPipeline(repo.value.id, pipelineOptions.value);