Skip to content

Commit

Permalink
Merge pull request #18729 from ElectronicBlueberry/workflow-editor-ac…
Browse files Browse the repository at this point in the history
…tivity-bar

Workflow Editor Activity Bar
  • Loading branch information
dannon authored Nov 26, 2024
2 parents 116a990 + 3fc286d commit ff7025d
Show file tree
Hide file tree
Showing 83 changed files with 3,466 additions and 1,745 deletions.
2 changes: 1 addition & 1 deletion client/src/components/ActivityBar/ActivityBar.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ describe("ActivityBar", () => {

beforeEach(async () => {
const pinia = createTestingPinia({ stubActions: false });
activityStore = useActivityStore();
activityStore = useActivityStore("default");
eventStore = useEventStore();
wrapper = shallowMount(mountTarget, {
localVue,
Expand Down
182 changes: 138 additions & 44 deletions client/src/components/ActivityBar/ActivityBar.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
<script setup lang="ts">
import { type IconDefinition } from "@fortawesome/fontawesome-svg-core";
import { faBell, faEllipsisH, faUserCog } from "@fortawesome/free-solid-svg-icons";
import { watchImmediate } from "@vueuse/core";
import { storeToRefs } from "pinia";
import { computed, type Ref, ref, watch } from "vue";
import { computed, type Ref, ref } from "vue";
import { useRoute } from "vue-router/composables";
import draggable from "vuedraggable";
import { useConfig } from "@/composables/config";
import { useHashedUserId } from "@/composables/hashedUserId";
import { convertDropData } from "@/stores/activitySetup";
import { type Activity, useActivityStore } from "@/stores/activityStore";
import { useEventStore } from "@/stores/eventStore";
Expand All @@ -24,6 +26,35 @@ import NotificationsPanel from "@/components/Panels/NotificationsPanel.vue";
import SettingsPanel from "@/components/Panels/SettingsPanel.vue";
import ToolPanel from "@/components/Panels/ToolPanel.vue";
const props = withDefaults(
defineProps<{
defaultActivities?: Activity[];
activityBarId?: string;
specialActivities?: Activity[];
showAdmin?: boolean;
optionsTitle?: string;
optionsTooltip?: string;
optionsHeading?: string;
optionsIcon?: IconDefinition;
optionsSearchPlaceholder?: string;
initialActivity?: string;
hidePanel?: boolean;
}>(),
{
defaultActivities: undefined,
activityBarId: "default",
specialActivities: () => [],
showAdmin: true,
optionsTitle: "More",
optionsHeading: "Additional Activities",
optionsIcon: () => faEllipsisH,
optionsSearchPlaceholder: "Search Activities",
optionsTooltip: "View additional activities",
initialActivity: undefined,
hidePanel: false,
}
);
// require user to long click before dragging
const DRAG_DELAY = 50;
Expand All @@ -32,13 +63,30 @@ const { config, isConfigLoaded } = useConfig();
const route = useRoute();
const userStore = useUserStore();
const { hashedUserId } = useHashedUserId();
const eventStore = useEventStore();
const activityStore = useActivityStore();
const activityStore = useActivityStore(props.activityBarId);
if (props.initialActivity) {
activityStore.toggledSideBar = props.initialActivity;
}
watchImmediate(
() => props.defaultActivities,
(defaults) => {
if (defaults) {
activityStore.overrideDefaultActivities(defaults);
} else {
activityStore.resetDefaultActivities();
}
}
);
const { isAdmin, isAnonymous } = storeToRefs(userStore);
const emit = defineEmits(["dragstart"]);
const emit = defineEmits<{
(e: "dragstart", dragItem: Activity | null): void;
(e: "activityClicked", activityId: string): void;
}>();
// activities from store
const { activities } = storeToRefs(activityStore);
Expand All @@ -50,30 +98,27 @@ const dragItem: Ref<Activity | null> = ref(null);
// drag state
const isDragging = ref(false);
// sync built-in activities with cached activities
activityStore.sync();
/**
* Checks if the route of an activity is currently being visited and panels are collapsed
*/
function isActiveRoute(activityTo: string) {
function isActiveRoute(activityTo?: string | null) {
return route.path === activityTo && isActiveSideBar("");
}
/**
* Checks if a panel has been expanded
*/
function isActiveSideBar(menuKey: string) {
return userStore.toggledSideBar === menuKey;
return activityStore.toggledSideBar === menuKey;
}
const isSideBarOpen = computed(() => userStore.toggledSideBar !== "");
const isSideBarOpen = computed(() => activityStore.toggledSideBar !== "");
/**
* Checks if an activity that has a panel should have the `is-active` prop
*/
function panelActivityIsActive(activity: Activity) {
return isActiveSideBar(activity.id) || (activity.to !== null && isActiveRoute(activity.to));
return isActiveSideBar(activity.id) || isActiveRoute(activity.to);
}
/**
Expand Down Expand Up @@ -123,25 +168,37 @@ function onDragOver(evt: MouseEvent) {
/**
* Tracks the state of activities which expand or collapse the sidepanel
*/
function onToggleSidebar(toggle: string = "", to: string | null = null) {
function toggleSidebar(toggle: string = "", to: string | null = null) {
// if an activity's dedicated panel/sideBar is already active
// but the route is different, don't collapse
if (toggle && to && !(route.path === to) && isActiveSideBar(toggle)) {
return;
}
userStore.toggleSideBar(toggle);
activityStore.toggleSideBar(toggle);
}
watch(
() => hashedUserId.value,
() => {
activityStore.sync();
function onActivityClicked(activity: Activity) {
if (activity.click) {
emit("activityClicked", activity.id);
} else {
toggleSidebar();
}
);
}
function setActiveSideBar(key: string) {
activityStore.toggledSideBar = key;
}
defineExpose({
isActiveSideBar,
setActiveSideBar,
});
</script>

<template>
<div class="d-flex">
<!-- while this warning is correct, it is hiding too many other errors -->
<!-- eslint-disable-next-line vuejs-accessibility/no-static-element-interactions -->
<div
class="activity-bar d-flex flex-column no-highlight"
data-description="activity bar"
Expand All @@ -163,79 +220,116 @@ watch(
<div v-if="activity.visible && (activity.anonymous || !isAnonymous)">
<UploadItem
v-if="activity.id === 'upload'"
:id="`activity-${activity.id}`"
:id="`${activity.id}`"
:key="activity.id"
:icon="activity.icon"
:title="activity.title"
:tooltip="activity.tooltip" />
<InteractiveItem
v-else-if="activity.to && activity.id === 'interactivetools'"
:id="`activity-${activity.id}`"
:id="`${activity.id}`"
:key="activity.id"
:icon="activity.icon"
:is-active="isActiveRoute(activity.to)"
:title="activity.title"
:tooltip="activity.tooltip"
:to="activity.to"
@click="onToggleSidebar()" />
@click="toggleSidebar()" />
<ActivityItem
v-else-if="activity.id === 'admin' || activity.panel"
:id="`activity-${activity.id}`"
:id="`${activity.id}`"
:key="activity.id"
:activity-bar-id="props.activityBarId"
:icon="activity.icon"
:is-active="panelActivityIsActive(activity)"
:title="activity.title"
:tooltip="activity.tooltip"
:to="activity.to || ''"
@click="onToggleSidebar(activity.id, activity.to)" />
@click="toggleSidebar(activity.id, activity.to)" />
<ActivityItem
v-else-if="activity.to"
:id="`activity-${activity.id}`"
v-else
:id="`${activity.id}`"
:key="activity.id"
:activity-bar-id="props.activityBarId"
:icon="activity.icon"
:is-active="isActiveRoute(activity.to)"
:title="activity.title"
:tooltip="activity.tooltip"
:to="activity.to"
@click="onToggleSidebar()" />
:to="activity.to ?? undefined"
:variant="activity.variant"
@click="onActivityClicked(activity)" />
</div>
</div>
</draggable>
</b-nav>
<b-nav v-if="!isAnonymous" vertical class="activity-footer flex-nowrap p-1">
<NotificationItem
v-if="isConfigLoaded && config.enable_notification_system"
id="activity-notifications"
icon="bell"
id="notifications"
:icon="faBell"
:is-active="isActiveSideBar('notifications') || isActiveRoute('/user/notifications')"
title="Notifications"
@click="onToggleSidebar('notifications')" />
@click="toggleSidebar('notifications')" />
<ActivityItem
id="activity-settings"
icon="ellipsis-h"
id="settings"
:activity-bar-id="props.activityBarId"
:icon="props.optionsIcon"
:is-active="isActiveSideBar('settings')"
title="More"
tooltip="View additional activities"
@click="onToggleSidebar('settings')" />
:title="props.optionsTitle"
:tooltip="props.optionsTooltip"
@click="toggleSidebar('settings')" />
<ActivityItem
v-if="isAdmin"
id="activity-admin"
icon="user-cog"
v-if="isAdmin && showAdmin"
id="admin"
:activity-bar-id="props.activityBarId"
:icon="faUserCog"
:is-active="isActiveSideBar('admin')"
title="Admin"
tooltip="Administer this Galaxy"
variant="danger"
@click="onToggleSidebar('admin')" />
@click="toggleSidebar('admin')" />
<template v-for="activity in props.specialActivities">
<ActivityItem
v-if="activity.panel"
:id="`${activity.id}`"
:key="activity.id"
:activity-bar-id="props.activityBarId"
:icon="activity.icon"
:is-active="panelActivityIsActive(activity)"
:title="activity.title"
:tooltip="activity.tooltip"
:to="activity.to || ''"
:variant="activity.variant"
@click="toggleSidebar(activity.id, activity.to)" />
<ActivityItem
v-else
:id="`${activity.id}`"
:key="activity.id"
:activity-bar-id="props.activityBarId"
:icon="activity.icon"
:is-active="isActiveRoute(activity.to)"
:title="activity.title"
:tooltip="activity.tooltip"
:to="activity.to ?? undefined"
:variant="activity.variant"
@click="onActivityClicked(activity)" />
</template>
</b-nav>
</div>
<FlexPanel v-if="isSideBarOpen" side="left" :collapsible="false">
<FlexPanel v-if="isSideBarOpen && !hidePanel" side="left" :collapsible="false">
<ToolPanel v-if="isActiveSideBar('tools')" />
<InvocationsPanel v-else-if="isActiveSideBar('invocation')" />
<InvocationsPanel v-else-if="isActiveSideBar('invocation')" :activity-bar-id="props.activityBarId" />
<VisualizationPanel v-else-if="isActiveSideBar('visualizations')" />
<MultiviewPanel v-else-if="isActiveSideBar('multiview')" />
<NotificationsPanel v-else-if="isActiveSideBar('notifications')" />
<SettingsPanel v-else-if="isActiveSideBar('settings')" />
<SettingsPanel
v-else-if="isActiveSideBar('settings')"
:activity-bar-id="props.activityBarId"
:heading="props.optionsHeading"
:search-placeholder="props.optionsSearchPlaceholder"
@activityClicked="(id) => emit('activityClicked', id)" />
<AdminPanel v-else-if="isActiveSideBar('admin')" />
<slot name="side-panel" :is-active-side-bar="isActiveSideBar"></slot>
</FlexPanel>
</div>
</template>
Expand Down
8 changes: 5 additions & 3 deletions client/src/components/ActivityBar/ActivityItem.test.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { createTestingPinia } from "@pinia/testing";
import { mount } from "@vue/test-utils";
import { getLocalVue } from "tests/jest/helpers";

Expand All @@ -12,6 +13,7 @@ describe("ActivityItem", () => {
wrapper = mount(mountTarget, {
propsData: {
id: "activity-test-id",
activityBarId: "activity-bar-test-id",
icon: "activity-test-icon",
indicator: 0,
progressPercentage: 0,
Expand All @@ -20,6 +22,7 @@ describe("ActivityItem", () => {
to: null,
tooltip: "activity-test-tooltip",
},
pinia: createTestingPinia(),
localVue,
stubs: {
FontAwesomeIcon: true,
Expand All @@ -28,8 +31,7 @@ describe("ActivityItem", () => {
});

it("rendering", async () => {
const reference = wrapper.find("[id='activity-test-id']");
expect(reference.attributes().id).toBe("activity-test-id");
const reference = wrapper.find(".activity-item");
expect(reference.text()).toBe("activity-test-title");
expect(reference.find("[icon='activity-test-icon']").exists()).toBeTruthy();
expect(reference.find(".progress").exists()).toBeFalsy();
Expand All @@ -45,7 +47,7 @@ describe("ActivityItem", () => {
});

it("rendering indicator", async () => {
const reference = wrapper.find("[id='activity-test-id']");
const reference = wrapper.find(".activity-item");
const indicatorSelector = "[data-description='activity indicator']";
const noindicator = reference.find(indicatorSelector);
expect(noindicator.exists()).toBeFalsy();
Expand Down
Loading

0 comments on commit ff7025d

Please sign in to comment.