Skip to content

Commit

Permalink
Merge branch 'release_23.1' into dev
Browse files Browse the repository at this point in the history
  • Loading branch information
mvdbeek committed Nov 20, 2023
2 parents 1666892 + 4659546 commit e26068f
Show file tree
Hide file tree
Showing 19 changed files with 310 additions and 86 deletions.
5 changes: 5 additions & 0 deletions client/src/components/Common/ButtonSpinner.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
variant="primary"
class="d-flex flex-nowrap align-items-center text-nowrap"
:title="tooltip"
:disabled="disabled"
@click="$emit('onClick')">
<FontAwesomeIcon icon="play" class="mr-2" />{{ title }}
</b-button>
Expand Down Expand Up @@ -43,6 +44,10 @@ export default {
type: String,
default: null,
},
disabled: {
type: Boolean,
default: false,
},
},
};
</script>
6 changes: 5 additions & 1 deletion client/src/components/Form/FormDisplay.vue
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ export default {
type: Boolean,
default: false,
},
allowEmptyValueOnRequiredInput: {
type: Boolean,
default: false,
},
},
data() {
return {
Expand All @@ -96,7 +100,7 @@ export default {
},
computed: {
validation() {
return validateInputs(this.formIndex, this.formData);
return validateInputs(this.formIndex, this.formData, this.allowEmptyValueOnRequiredInput);
},
},
watch: {
Expand Down
8 changes: 5 additions & 3 deletions client/src/components/Form/utilities.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export function matchInputs(index, response) {
* @param{dict} index - Index of input elements
* @param{dict} values - Dictionary of parameter values
*/
export function validateInputs(index, values) {
export function validateInputs(index, values, allowEmptyValueOnRequiredInput = false) {
let batchN = -1;
let batchSrc = null;
for (const inputId in values) {
Expand All @@ -113,8 +113,10 @@ export function validateInputs(index, values) {
if (!inputDef || inputDef.step_linked) {
continue;
}
if (inputValue == null && !inputDef.optional && inputDef.type != "hidden") {
return [inputId, "Please provide a value for this option."];
if (!inputDef.optional && inputDef.type != "hidden") {
if (inputValue == null || (allowEmptyValueOnRequiredInput && inputValue === "")) {
return [inputId, "Please provide a value for this option."];
}
}
if (inputDef.wp_linked && inputDef.text_value == inputValue) {
return [inputId, "Please provide a value for this workflow parameter."];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,26 @@ import BroadcastsOverlay from "./BroadcastsOverlay.vue";
const localVue = getLocalVue(true);

const now = new Date();
const inTwoMonths = new Date(now.setMonth(now.getMonth() + 2));
const inTwoMonths = new Date(new Date(now).setMonth(now.getMonth() + 2));

let idCounter = 0;

function generateBroadcastNotification(overwrites: Partial<BroadcastNotification> = {}): BroadcastNotification {
const id = `${idCounter++}`;
/** API date-time does not have timezone indicator and it's always UTC. */
function toApiDate(date: Date): string {
return date.toISOString().replace("Z", "");
}

function generateBroadcastNotification(
id: string,
publicationTime?: Date,
expirationTime?: Date
): BroadcastNotification {
const publication_time = publicationTime ? toApiDate(publicationTime) : toApiDate(now);
const expiration_time = expirationTime ? toApiDate(expirationTime) : toApiDate(inTwoMonths);
return {
id: id,
create_time: now.toISOString(),
update_time: now.toISOString(),
publication_time: now.toISOString(),
expiration_time: inTwoMonths.toISOString(),
create_time: toApiDate(now),
update_time: toApiDate(now),
publication_time,
expiration_time,
source: "testing",
variant: "info",
content: {
Expand Down Expand Up @@ -137,4 +144,20 @@ describe("BroadcastsOverlay.vue", () => {
await leftButton.trigger("click");
expect(wrapper.find(messageCssSelector).text()).toContain("Test message 2");
});

it("should not render the broadcast when it has expired", async () => {
const expiredBroadcast = generateBroadcastNotification("expired", undefined, new Date(now));
const wrapper = await mountBroadcastsOverlayWith([expiredBroadcast]);

expect(wrapper.exists()).toBe(true);
expect(wrapper.html()).toBe("");
});

it("should not render the broadcast when it has not been published yet", async () => {
const unpublishedBroadcast = generateBroadcastNotification("unpublished", new Date(inTwoMonths));
const wrapper = await mountBroadcastsOverlayWith([unpublishedBroadcast]);

expect(wrapper.exists()).toBe(true);
expect(wrapper.html()).toBe("");
});
});
43 changes: 34 additions & 9 deletions client/src/components/Workflow/Run/WorkflowRunFormSimple.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
<template>
<div>
<div class="h4 clearfix mb-3">
<div v-if="isConfigLoaded" class="h4 clearfix mb-3">
<b>Workflow: {{ model.name }}</b>
<ButtonSpinner id="run-workflow" class="float-right" title="Run Workflow" @onClick="onExecute" />
<ButtonSpinner
id="run-workflow"
:wait="waitingForRequest"
:disabled="hasValidationErrors"
class="float-right"
title="Run Workflow"
@onClick="onExecute" />
<b-dropdown
v-if="showRuntimeSettings(currentUser)"
id="dropdown-form"
Expand Down Expand Up @@ -39,7 +45,11 @@
</b-dropdown-form>
</b-dropdown>
</div>
<FormDisplay :inputs="formInputs" @onChange="onChange" />
<FormDisplay
:inputs="formInputs"
:allow-empty-value-on-required-input="true"
@onChange="onChange"
@onValidation="onValidation" />
<!-- Options to default one way or the other, disable if admins want, etc.. -->
<a href="#" class="workflow-expand-form-link" @click="$emit('showAdvanced')">Expand to full workflow form.</a>
</div>
Expand All @@ -50,8 +60,9 @@ import ButtonSpinner from "components/Common/ButtonSpinner";
import FormDisplay from "components/Form/FormDisplay";
import { allowCachedJobs } from "components/Tool/utilities";
import { isWorkflowInput } from "components/Workflow/constants";
import { mapState } from "pinia";
import { storeToRefs } from "pinia";
import { errorMessageAsString } from "utils/simple-error";
import Vue from "vue";
import { useConfig } from "@/composables/config";
import { useUserStore } from "@/stores/userStore";
Expand Down Expand Up @@ -81,22 +92,24 @@ export default {
},
setup() {
const { config, isConfigLoaded } = useConfig(true);
return { config, isConfigLoaded };
const { currentUser } = storeToRefs(useUserStore());
return { config, isConfigLoaded, currentUser };
},
data() {
const newHistory = this.targetHistory == "new" || this.targetHistory == "prefer_new";
return {
formData: {},
inputTypes: {},
stepValidations: {},
sendToNewHistory: newHistory,
useCachedJobs: this.useJobCache, // TODO:
splitObjectStore: false,
preferredObjectStoreId: null,
preferredIntermediateObjectStoreId: null,
waitingForRequest: false,
};
},
computed: {
...mapState(useUserStore, ["currentUser"]),
formInputs() {
const inputs = [];
// Add workflow parameters.
Expand Down Expand Up @@ -127,13 +140,23 @@ export default {
});
return inputs;
},
hasValidationErrors() {
return Boolean(Object.values(this.stepValidations).find((value) => value !== null && value !== undefined));
},
},
methods: {
onValidation(validation) {
if (validation) {
Vue.set(this.stepValidations, validation[0], validation[1]);
} else {
this.stepValidations = {};
}
},
reuseAllowed(user) {
return allowCachedJobs(user.preferences);
return user && allowCachedJobs(user.preferences);
},
showRuntimeSettings(user) {
return this.targetHistory.indexOf("prefer") >= 0 || this.reuseAllowed(user);
return this.targetHistory.indexOf("prefer") >= 0 || (user && this.reuseAllowed(user));
},
onChange(data) {
this.formData = data;
Expand Down Expand Up @@ -182,13 +205,15 @@ export default {
data.preferred_object_store_id = this.preferredObjectStoreId;
}
}
this.waitingForRequest = true;
invokeWorkflow(this.model.workflowId, data)
.then((invocations) => {
this.$emit("submissionSuccess", invocations);
})
.catch((error) => {
this.$emit("submissionError", errorMessageAsString(error));
});
})
.finally(() => (this.waitingForRequest = false));
},
},
};
Expand Down
125 changes: 73 additions & 52 deletions client/src/stores/broadcastsStore.ts
Original file line number Diff line number Diff line change
@@ -1,71 +1,92 @@
import { defineStore } from "pinia";
import { computed, ref, set } from "vue";
import Vue, { computed, ref } from "vue";

import { fetchAllBroadcasts } from "@/api/notifications.broadcast";
import type { components } from "@/api/schema";
import { useUserLocalStorage } from "@/composables/userLocalStorage";
import type { components } from "@/schema";
import { loadBroadcastsFromServer } from "@/stores/services/broadcasts.service";
import { mergeObjectListsById } from "@/utils/utils";

export type BroadcastNotification = components["schemas"]["BroadcastNotificationResponse"];
type Expirable = Pick<BroadcastNotification, "expiration_time">;

export const useBroadcastsStore = defineStore("broadcastsStore", () => {
const broadcasts = ref<BroadcastNotification[]>([]);
export const useBroadcastsStore = defineStore(
"broadcastsStore",
() => {
const broadcasts = ref<BroadcastNotification[]>([]);

const loadingBroadcasts = ref<boolean>(false);
const dismissedBroadcasts = useUserLocalStorage<{ [key: string]: Expirable }>("dismissed-broadcasts", {});
const loadingBroadcasts = ref<boolean>(false);
const dismissedBroadcasts = ref<{ [key: string]: Expirable }>({});

const activeBroadcasts = computed(() => {
return broadcasts.value.filter((b) => !dismissedBroadcasts.value[b.id]);
});
const activeBroadcasts = computed(() => {
return broadcasts.value.filter(isActive);
});

async function loadBroadcasts() {
loadingBroadcasts.value = true;
await fetchAllBroadcasts()
.then((data) => {
broadcasts.value = mergeObjectListsById(data, [], "create_time", "desc");
})
.finally(() => {
loadingBroadcasts.value = false;
});
}
async function loadBroadcasts() {
loadingBroadcasts.value = true;
await loadBroadcastsFromServer()
.then((data) => {
broadcasts.value = mergeObjectListsById(data, [], "create_time", "desc");
})
.finally(() => {
loadingBroadcasts.value = false;
});
}

function updateBroadcasts(broadcastList: BroadcastNotification[]) {
broadcasts.value = mergeObjectListsById(broadcasts.value, broadcastList, "create_time", "desc").filter(
(b) => !hasExpired(b.expiration_time)
);
}
function updateBroadcasts(broadcastList: BroadcastNotification[]) {
broadcasts.value = mergeObjectListsById(broadcasts.value, broadcastList, "create_time", "desc").filter(
(b) => !hasExpired(b.expiration_time)
);
}

function dismissBroadcast(broadcast: BroadcastNotification) {
set(dismissedBroadcasts.value, broadcast.id, { expiration_time: broadcast.expiration_time });
}
function dismissBroadcast(broadcast: BroadcastNotification) {
Vue.set(dismissedBroadcasts.value, broadcast.id, { expiration_time: broadcast.expiration_time });
}

function hasExpired(expirationTimeStr?: string) {
if (!expirationTimeStr) {
return false;
function isActive(broadcast: BroadcastNotification) {
return (
!dismissedBroadcasts.value[broadcast.id] &&
!hasExpired(broadcast.expiration_time) &&
hasBeenPublished(broadcast)
);
}
const expirationTime = new Date(`${expirationTimeStr}Z`);
const now = new Date();
return now > expirationTime;
}

function clearExpiredDismissedBroadcasts() {
for (const key in dismissedBroadcasts.value) {
if (hasExpired(dismissedBroadcasts.value[key]?.expiration_time)) {
delete dismissedBroadcasts.value[key];
function hasExpired(expirationTimeStr?: string) {
if (!expirationTimeStr) {
return false;
}
const expirationTime = new Date(`${expirationTimeStr}Z`);
const now = new Date();
return now > expirationTime;
}
}

clearExpiredDismissedBroadcasts();
function hasBeenPublished(broadcast: BroadcastNotification) {
const publicationTime = new Date(`${broadcast.publication_time}Z`);
const now = new Date();
return now >= publicationTime;
}

return {
broadcasts,
dismissedBroadcasts,
loadingBroadcasts,
activeBroadcasts,
dismissBroadcast,
loadBroadcasts,
updateBroadcasts,
};
});
function clearExpiredDismissedBroadcasts() {
for (const key in dismissedBroadcasts.value) {
if (hasExpired(dismissedBroadcasts.value[key]?.expiration_time)) {
delete dismissedBroadcasts.value[key];
}
}
}

clearExpiredDismissedBroadcasts();

return {
broadcasts,
dismissedBroadcasts,
loadingBroadcasts,
activeBroadcasts,
dismissBroadcast,
loadBroadcasts,
updateBroadcasts,
};
},
{
persist: {
paths: ["dismissedBroadcasts"],
},
}
);
4 changes: 4 additions & 0 deletions client/src/utils/navigation/navigation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -653,12 +653,16 @@ workflow_run:
# TODO: put step labels in the DOM ideally
subworkflow_step_icon: ".portlet-title-icon.fa-sitemap"
run_workflow: "#run-workflow"
run_workflow_disabled: "#run-workflow.disabled"
validation_error: ".validation-error"
expand_form_link: '.workflow-expand-form-link'
expanded_form: '.workflow-expanded-form'
new_history_target_link: '.workflow-new-history-target-link'
runtime_setting_button: '.workflow-run-settings'
runtime_setting_target: '.workflow-run-settings-target'
simplified_input:
type: xpath
selector: //div[@data-label="${label}"]//input
input_select_field:
type: xpath
selector: '//div[@data-label="${label}"]//div[contains(@class, "multiselect")]'
Expand Down
Loading

0 comments on commit e26068f

Please sign in to comment.