Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"@types/jest": "^29.5.12",
"@vueuse/core": "^10.5.0",
"@vueuse/math": "^10.9.0",
"ace-builds": "^1.39.0",
"assert": "^2.1.0",
"axios": "^1.6.2",
"babel-runtime": "^6.26.0",
Expand Down
91 changes: 91 additions & 0 deletions client/src/components/Markdown/Editor/CellAction.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<template>
<div>
<CellButton ref="buttonRef" title="Actions" :show="show" :icon="faEllipsisV" />
<Popper
v-if="buttonRef"
ref="popperRef"
:reference-el="buttonRef.$el"
trigger="click"
placement="right"
mode="light">
<div @click="popperRef.visible = false">
<span class="d-flex justify-content-between">
<small class="my-1 mx-3 text-info">{{ title }}</small>
</span>
<CellOption
v-if="name !== 'markdown'"
title="Attach Data"
description="Select data for this cell"
:icon="faPaperclip"
@click="$emit('configure')" />
<CellOption
title="Clone"
description="Create a copy of this cell"
:icon="faClone"
@click="$emit('clone')" />
<CellOption
title="Delete"
description="Delete this cell"
:icon="faTrash"
@click="confirmDelete = true" />
<CellOption
v-if="cellIndex > 0"
title="Move Up"
description="Move this cell upwards"
:icon="faArrowUp"
@click="$emit('move', 'up')" />
<CellOption
v-if="cellTotal - cellIndex > 1"
title="Move Down"
description="Move this cell downwards"
:icon="faArrowDown"
@click="$emit('move', 'down')" />
</div>
</Popper>
<BModal v-model="confirmDelete" title="Delete Cell" title-tag="h2" @ok="$emit('delete')">
<p v-localize>Are you sure you want to delete this cell?</p>
</BModal>
</div>
</template>

<script setup lang="ts">
import { faClone, faEllipsisV } from "@fortawesome/free-solid-svg-icons";
import { BModal } from "bootstrap-vue";
import { faArrowDown, faArrowUp, faPaperclip, faTrash } from "font-awesome-6";
import { computed, ref } from "vue";

import type { CellType } from "./types";

import CellButton from "./CellButton.vue";
import CellOption from "./CellOption.vue";
import Popper from "@/components/Popper/Popper.vue";

const props = defineProps<{
cellIndex: number;
cellTotal: number;
name: string;
show: boolean;
}>();

defineEmits<{
(e: "click", cell: CellType): void;
(e: "clone"): void;
(e: "configure"): void;
(e: "delete"): void;
(e: "move", direction: string): void;
}>();

const buttonRef = ref();
const confirmDelete = ref(false);
const popperRef = ref();

const title = computed(() => `${props.name.charAt(0).toUpperCase()}${props.name.slice(1)}`);
</script>

<style>
.cell-add-categories {
max-height: 20rem;
max-width: 15rem;
min-width: 15rem;
}
</style>
88 changes: 88 additions & 0 deletions client/src/components/Markdown/Editor/CellAdd.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { mount } from "@vue/test-utils";
import { BAlert } from "bootstrap-vue";

import CellAdd from "./CellAdd.vue";
import CellButton from "./CellButton.vue";
import CellOption from "./CellOption.vue";
import DelayedInput from "@/components/Common/DelayedInput.vue";
import Popper from "@/components/Popper/Popper.vue";

jest.mock("./templates", () => ({
cellTemplates: [
{
name: "Category 1",
templates: [
{ title: "Option A", description: "Desc A", cell: { id: 1 } },
{ title: "Option B", description: "Desc B", cell: { id: 2 } },
],
},
{
name: "Category 2",
templates: [{ title: "Option C", description: "Desc C", cell: { id: 3 } }],
},
],
}));

const createContainer = (tag = "div") => {
const container = document.createElement(tag);
document.body.appendChild(container);
return container;
};

const mountTarget = () => {
return mount(CellAdd, {
attachTo: createContainer(),
global: {
components: { BAlert, CellButton, CellOption, DelayedInput, Popper },
},
});
};

describe("CellAdd.vue", () => {
it("renders correctly", async () => {
const wrapper = mountTarget();
expect(wrapper.exists()).toBe(true);
expect(wrapper.findComponent(CellButton).exists()).toBe(true);
});

it("opens the popper when clicking the button", async () => {
const wrapper = mountTarget();
await wrapper.findComponent(CellButton).trigger("click");
await wrapper.vm.$nextTick();
expect(wrapper.findComponent(Popper).exists()).toBe(true);
});

it("filters templates based on search input", async () => {
const wrapper = mountTarget();
await wrapper.vm.$nextTick();
wrapper.findComponent(DelayedInput).vm.$emit("change", "option a");
await wrapper.vm.$nextTick();
const categories = wrapper.findAll(".cell-add-categories");
expect(categories).toHaveLength(1);
expect(categories.at(0).find(".text-info").text()).toBe("Category 1");
expect(categories.at(0).find(".cell-option").text()).toContain("Option A");
});

it("shows 'No results found' when no templates match search", async () => {
const wrapper = mountTarget();
await wrapper.vm.$nextTick();
wrapper.findComponent(DelayedInput).vm.$emit("change", "nonexistent");
await wrapper.vm.$nextTick();
expect(wrapper.findComponent(BAlert).exists()).toBe(true);
expect(wrapper.findComponent(BAlert).text()).toContain('No results found for "nonexistent".');
});

it("emits a 'click' event when a cell option is selected", async () => {
const wrapper = mountTarget();
await wrapper.findComponent(CellButton).trigger("click");
await wrapper.vm.$nextTick();
const option = wrapper.findComponent(CellOption);
await option.trigger("click");
expect(wrapper.emitted("click")).toBeTruthy();
expect(wrapper.emitted("click")?.[0][0]).toMatchObject({
configure: false,
toggle: true,
id: expect.any(Number),
});
});
});
75 changes: 75 additions & 0 deletions client/src/components/Markdown/Editor/CellAdd.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<template>
<div>
<CellButton ref="buttonRef" title="Insert" :icon="faPlus" />
<Popper v-if="buttonRef" :reference-el="buttonRef.$el" trigger="click" placement="right" mode="light">
<DelayedInput class="p-1" :delay="100" placeholder="Search" @change="query = $event" />
<div class="cell-add-categories overflow-auto">
<div v-if="filteredTemplates.length > 0">
<div v-for="(category, categoryIndex) of filteredTemplates" :key="categoryIndex">
<hr class="solid m-0" />
<span class="d-flex justify-content-between">
<small class="my-1 mx-3 text-info">{{ category.name }}</small>
</span>
<div v-if="category.templates.length > 0" class="cell-add-options popper-close">
<CellOption
v-for="(option, optionIndex) of category.templates"
:key="optionIndex"
:title="option.title"
:description="option.description"
@click="$emit('click', { configure: false, toggle: true, ...option.cell })" />
</div>
</div>
</div>
<BAlert v-else class="m-1 p-1" variant="info" show> No results found for "{{ query }}". </BAlert>
</div>
</Popper>
</div>
</template>

<script setup lang="ts">
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { BAlert } from "bootstrap-vue";
import { computed, ref } from "vue";

import { cellTemplates } from "./templates";
import type { CellType, TemplateCategory } from "./types";

import CellButton from "./CellButton.vue";
import CellOption from "./CellOption.vue";
import DelayedInput from "@/components/Common/DelayedInput.vue";
import Popper from "@/components/Popper/Popper.vue";

defineEmits<{
(e: "click", cell: CellType): void;
}>();

const buttonRef = ref();
const query = ref("");

const filteredTemplates = computed(() => {
const filteredCategories: Array<TemplateCategory> = [];
cellTemplates.forEach((category) => {
const matchedTemplates = category.templates.filter(
(template) =>
category.name.toLowerCase().includes(query.value.toLowerCase()) ||
template.title.toLowerCase().includes(query.value.toLowerCase()) ||
template.description.toLowerCase().includes(query.value.toLowerCase())
);
if (matchedTemplates.length > 0) {
filteredCategories.push({
name: category.name,
templates: matchedTemplates,
});
}
});
return filteredCategories;
});
</script>

<style>
.cell-add-categories {
max-height: 20rem;
max-width: 15rem;
min-width: 15rem;
}
</style>
38 changes: 38 additions & 0 deletions client/src/components/Markdown/Editor/CellButton.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { mount } from "@vue/test-utils";
import { getLocalVue } from "tests/jest/helpers";

import Target from "./CellButton.vue";

const localVue = getLocalVue();

function mountTarget(props = {}) {
return mount(Target, {
localVue,
propsData: props,
stubs: {
FontAwesomeIcon: true,
},
});
}

describe("CellButton.vue", () => {
it("should render button", async () => {
const wrapper = mountTarget({
icon: "button-icon",
title: "button-title",
});
expect(wrapper.find("[icon='button-icon']").exists()).toBeTruthy();
expect(wrapper.attributes()["title"]).toBe("button-title");
expect(wrapper.classes()).not.toContain("active");
expect(wrapper.classes()).toContain("btn-outline-primary");
await wrapper.setProps({ active: true });
expect(wrapper.classes()).toContain("active");
expect(wrapper.classes()).toContain("btn-outline-secondary");
await wrapper.trigger("click");
expect(wrapper.emitted("click")).toBeTruthy();
expect(wrapper.emitted("click")?.length).toBe(1);
wrapper.element.blur = jest.fn();
await wrapper.trigger("mouseleave");
expect(wrapper.element.blur).toHaveBeenCalled();
});
});
49 changes: 49 additions & 0 deletions client/src/components/Markdown/Editor/CellButton.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<template>
<BButton
v-b-tooltip.noninteractive.right
class="border-0 m-1 px-1 py-0"
:class="{ active, 'cell-button-hide': !show }"
:title="title"
:variant="active ? 'outline-secondary' : 'outline-primary'"
@click="$emit('click')"
@mouseleave="onMouseLeave($event)">
<FontAwesomeIcon :icon="icon" fixed-width />
</BButton>
</template>

<script setup lang="ts">
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { BButton, VBTooltipPlugin } from "bootstrap-vue";
import type { IconDefinition } from "font-awesome-6";
import Vue from "vue";

Vue.use(VBTooltipPlugin);

withDefaults(
defineProps<{
active?: boolean;
icon: IconDefinition;
show?: boolean;
title: string;
}>(),
{
active: false,
show: true,
}
);

defineEmits<{
(e: "click"): void;
}>();

function onMouseLeave(event: Event) {
const target = event.target as HTMLElement;
target.blur();
}
</script>

<style>
.cell-button-hide {
color: transparent !important;
}
</style>
Loading
Loading