Skip to content
Snippets Groups Projects

Resolve "Update Workflow Modal"

Merged Daniel Göbel requested to merge feature/45-update-workflow-modal into development
3 files
+ 63
23
Compare changes
  • Side-by-side
  • Inline
Files
3
<script setup lang="ts">
import { computed, onMounted, reactive, ref, watch } from "vue";
import { Modal, Toast } from "bootstrap";
import type {
Body_Workflow_update_workflow,
WorkflowOut,
WorkflowVersionFull,
} from "@/client/workflow";
import BootstrapModal from "@/components/modals/BootstrapModal.vue";
import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue";
import type { WorkflowVersionReduced, ApiError } from "@/client/workflow";
import { WorkflowService } from "@/client/workflow";
import {
GitRepository,
requiredRepositoryFiles,
determineGitIcon,
} from "@/utils/GitRepository";
import { valid, lte, inc } from "semver";
import { latestVersion as calculateLatestVersion } from "@/utils/Workflow";
// Bootstrap Elements
// =============================================================================
let updateWorkflowModal: Modal | null = null;
let successToast: Toast | null = null;
// Form Elements
// =============================================================================
const workflowUpdateForm = ref<HTMLFormElement | undefined>(undefined);
const workflowIconInputElement = ref<HTMLInputElement | undefined>(undefined);
const workflowVersionElement = ref<HTMLInputElement | undefined>(undefined);
const workflowGitCommitHashElement = ref<HTMLInputElement | undefined>(
undefined
);
const workflowIconElement = ref<HTMLImageElement | undefined>(undefined);
// Constants
// =============================================================================
const randomIDSuffix = Math.random().toString(16).substr(2, 8);
// Props
// =============================================================================
const props = defineProps<{
modalID: string;
workflow: WorkflowOut;
}>();
// Reactive State
// =============================================================================
const workflowUpdate = reactive<Body_Workflow_update_workflow>({
icon: undefined,
version: "",
git_commit_hash: "",
});
const formState = reactive<{
validated: boolean;
missingFiles: string[];
loading: boolean;
checkRepoLoading: boolean;
allowUpload: boolean;
}>({
loading: false,
checkRepoLoading: false,
allowUpload: false,
validated: false,
missingFiles: [],
});
watch(
() => props.workflow,
() => {
resetForm();
}
);
// Computed Properties
// =============================================================================
const latestVersion = computed<WorkflowVersionReduced>(() => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return calculateLatestVersion(props.workflow.versions)!;
});
const gitIcon = computed<string>(() =>
determineGitIcon(props.workflow.repository_url)
);
const showIcon = computed<boolean>(
() =>
latestVersion.value.icon_url != undefined ||
workflowUpdate.icon != undefined
);
// Emitted Events
// =============================================================================
const emit = defineEmits<{
(e: "workflow-updated", workflow: WorkflowVersionFull): void;
}>();
// Functions
// =============================================================================
function iconChanged() {
workflowUpdate.icon = workflowIconInputElement.value?.files?.[0].slice();
if (workflowUpdate.icon != undefined) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
workflowIconElement.value!.src = URL.createObjectURL(
workflowUpdate.icon.slice()
);
} else {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
workflowIconElement.value!.src = latestVersion.value.icon_url ?? "";
}
}
function modalClosed() {
formState.validated = false;
formState.missingFiles = [];
formState.allowUpload = false;
workflowGitCommitHashElement.value?.setCustomValidity("");
}
function checkVersionValidity() {
if (valid(workflowUpdate.version) == null) {
workflowVersionElement.value?.setCustomValidity(
"Please use semantic versioning"
);
} else if (lte(workflowUpdate.version, latestVersion.value.version)) {
workflowVersionElement.value?.setCustomValidity(
"The new version must be greater than previous version"
);
} else {
workflowVersionElement.value?.setCustomValidity("");
}
}
function checkRepository() {
formState.validated = true;
if (workflowUpdateForm.value?.checkValidity() && !formState.allowUpload) {
formState.missingFiles = [];
workflowGitCommitHashElement.value?.setCustomValidity("");
const repo = GitRepository.buildRepository(
props.workflow.repository_url,
workflowUpdate.git_commit_hash
);
repo
.checkFilesExist(requiredRepositoryFiles, true)
.then(() => {
formState.allowUpload = true;
})
.catch((e: Error) => {
formState.missingFiles = e.message.split(",");
workflowGitCommitHashElement.value?.setCustomValidity(
"Files are missing in the repository"
);
});
}
}
function resetForm() {
modalClosed();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
workflowIconElement.value!.src = latestVersion.value.icon_url ?? "";
workflowUpdate.version = "";
workflowUpdate.icon = undefined;
workflowUpdate.git_commit_hash = "";
if (workflowIconInputElement.value != undefined) {
workflowIconInputElement.value.value = "";
}
}
function updateWorkflow() {
formState.validated = true;
workflowUpdate.version = workflowUpdate.version.trim();
if (workflowUpdateForm.value?.checkValidity() && formState.allowUpload) {
formState.loading = true;
workflowGitCommitHashElement.value?.setCustomValidity("");
WorkflowService.workflowUpdateWorkflow(
props.workflow.workflow_id,
workflowUpdate
)
.then((version) => {
emit("workflow-updated", version);
successToast?.show();
updateWorkflowModal?.hide();
resetForm();
})
.catch((error: ApiError) => {
const errorText = error.body["detail"];
if (errorText.startsWith("Workflow with git_commit_hash")) {
workflowGitCommitHashElement.value?.setCustomValidity(
"Git commit is already used by a workflow"
);
}
})
.finally(() => {
formState.loading = false;
});
}
}
// Lifecycle Events
// =============================================================================
onMounted(() => {
updateWorkflowModal = new Modal("#" + props.modalID);
successToast = new Toast("#successToast-" + randomIDSuffix);
});
</script>
<template>
<div class="toast-container position-fixed top-toast end-0 p-3">
<div
role="alert"
aria-live="assertive"
aria-atomic="true"
class="toast text-bg-success align-items-center border-0"
data-bs-autohide="true"
:id="'successToast-' + randomIDSuffix"
>
<div class="d-flex">
<div class="toast-body">Successfully updated Workflow</div>
<button
type="button"
class="btn-close btn-close-white me-2 m-auto"
data-bs-dismiss="toast"
aria-label="Close"
></button>
</div>
</div>
</div>
<bootstrap-modal
:modalID="modalID"
:static-backdrop="true"
modal-label="Update Workflow Modal"
v-on="{ 'hidden.bs.modal': modalClosed }"
>
<template v-slot:header>
Update Workflow
<span class="fw-bold">{{ props.workflow.name }}</span>
</template>
<template v-slot:body>
<form
id="workflowUpdateForm"
:class="{ 'was-validated': formState.validated }"
ref="workflowUpdateForm"
>
<div class="mb-3">
<span class="me-3">Git Repository URL:</span>
<font-awesome-icon :icon="gitIcon" />
<a
class="ms-2"
:href="props.workflow.repository_url"
target="_blank"
>{{ props.workflow.repository_url }}</a
>
<img
:src="latestVersion.icon_url"
ref="workflowIconElement"
class="float-end"
:hidden="!showIcon"
/>
</div>
<div class="mb-3">
<label for="workflowGitCommitInput" class="form-label"
>Git Commit Hash</label
>
<div class="input-group">
<div class="input-group-text">
<font-awesome-icon icon="fa-solid fa-code-commit" />
</div>
<input
type="text"
class="form-control text-lowercase"
id="workflowGitCommitInput"
placeholder="ba8bcd9..."
required
ref="workflowGitCommitHashElement"
maxlength="40"
pattern="[0-9a-f]{40}"
v-model="workflowUpdate.git_commit_hash"
@change="formState.allowUpload = false"
/>
</div>
</div>
<div v-if="formState.missingFiles.length > 0" class="text-danger">
The following files are missing in the repository
<ul>
<li v-for="file in formState.missingFiles" :key="file">
{{ file }}
</li>
</ul>
</div>
<div class="row mb-3">
<div class="col-4">
<label for="workflowVersionInput" class="form-label">Version</label>
<div class="input-group">
<div class="input-group-text">
<font-awesome-icon icon="fa-solid fa-tag" />
</div>
<input
type="text"
class="form-control"
id="workflowRepositoryInput"
:placeholder="inc(latestVersion.version, 'patch') ?? undefined"
maxlength="10"
required
ref="workflowVersionElement"
@change="checkVersionValidity"
v-model="workflowUpdate.version"
aria-describedby="versionHelp"
/>
</div>
<div id="versionHelp" class="form-text">
Previous Version: {{ latestVersion.version }}
</div>
</div>
<div class="col-8">
<label for="workflowIconInput" class="form-label"
>Optional Icon</label
>
<input
type="file"
ref="workflowIconInputElement"
accept="image/*"
class="form-control"
id="workflowIconInput"
@change="iconChanged"
aria-describedby="iconHelp"
/>
<div id="iconHelp" class="form-text">
If not set, the previous icon will be used
</div>
</div>
</div>
</form>
</template>
<template v-slot:footer>
<button
type="button"
class="btn btn-info me-auto"
@click="checkRepository"
:disabled="formState.allowUpload"
>
<span
v-if="formState.checkRepoLoading"
class="spinner-border spinner-border-sm"
role="status"
aria-hidden="true"
></span>
Check Repository
</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
Close
</button>
<button
type="submit"
form="workflowUpdateForm"
class="btn btn-primary"
:disabled="formState.loading || !formState.allowUpload"
@click.prevent="updateWorkflow"
>
<span
v-if="formState.loading"
class="spinner-border spinner-border-sm"
role="status"
aria-hidden="true"
></span>
Save
</button>
</template>
</bootstrap-modal>
</template>
<style scoped>
img {
max-height: 32px;
max-width: 32px;
}
</style>
Loading