Skip to content

Commit

Permalink
[studio] add db actions with user input #1431 (#1448)
Browse files Browse the repository at this point in the history
* studio - action inputs

* studio - user input

* studio - db width

* studio - unit tests
  • Loading branch information
janavlachova authored Jan 3, 2025
1 parent 80cddc0 commit 36e0e79
Show file tree
Hide file tree
Showing 23 changed files with 591 additions and 54 deletions.
4 changes: 2 additions & 2 deletions agdb_studio/src/assets/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
a {
text-decoration: none;
color: #ff8800;
transition: 0.4s;
transition: 0.2s;
}

h1,
Expand All @@ -18,7 +18,7 @@ h4,
h5,
h6 {
font-family: var(--base-font);
text-wrap-style: balance;
text-wrap: balance;
}

h1 {
Expand Down
92 changes: 92 additions & 0 deletions agdb_studio/src/components/base/content/AgdbContent.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { describe, beforeEach, vi, it, expect } from "vitest";
import AgdbContent from "./AgdbContent.vue";
import { mount } from "@vue/test-utils";
import { useContentInputs } from "@/composables/content/inputs";
import { ref } from "vue";

const { addInput, getInputValue, clearAllInputs } = useContentInputs();

describe("AgdbContent", () => {
const testKey = Symbol("test");
beforeEach(() => {
vi.clearAllMocks();
clearAllInputs();
});

it("renders the content", () => {
const wrapper = mount(AgdbContent, {
props: {
content: [
{
paragraph: [
{
text: "Test Body",
},
],
},
{
component: "my-component",
},
{
input: {
key: "test",
type: "text",
label: "Test input",
},
},
],
contentKey: testKey,
},
});
expect(wrapper.html()).toContain("Test Body");
expect(wrapper.html()).toContain("my-component");
expect(wrapper.html()).toContain("Test input");
});
it("change the input value on user input", async () => {
const inputValue = ref("");
addInput(testKey, "test", inputValue);
const wrapper = mount(AgdbContent, {
props: {
content: [
{
input: {
key: "test",
type: "text",
label: "Test input",
},
},
],
contentKey: testKey,
},
});
const input = wrapper.find("input");
expect(getInputValue(testKey, "test")).toBe("");
expect(input.element.value).toBe("");
input.element.value = "test value";
await input.trigger("input");
expect(getInputValue(testKey, "test")).toBe("test value");
});
it("sets focus on the input with autofocus", async () => {
const inputValue = ref("");
addInput(testKey, "test", inputValue);
const wrapper = mount(AgdbContent, {
props: {
content: [
{
input: {
key: "test",
type: "text",
label: "Test input",
autofocus: true,
},
},
],
contentKey: testKey,
},
attachTo: document.body,
});
await wrapper.vm.$nextTick();
const input = wrapper.find("input");
expect(input.element.matches(":focus")).toBe(true);
});
});
71 changes: 71 additions & 0 deletions agdb_studio/src/components/base/content/AgdbContent.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<script lang="ts" setup>
import { onMounted, ref, type PropType } from "vue";
import { useContentInputs } from "@/composables/content/inputs";
const props = defineProps({
content: { type: Array as PropType<Content[]>, required: true },
contentKey: { type: Symbol, required: true },
});
const { getContentInputs, setInputValue } = useContentInputs();
const inputs = getContentInputs(props.contentKey) ?? new Map();
const autofocusElement = ref();
onMounted(() => {
autofocusElement.value?.focus();
});
</script>

<template>
<div class="agdb-content">
<div v-for="(part, index) in content" :key="index">
<p v-if="part.paragraph?.length">
<span
v-for="(text, index2) in part.paragraph"
:key="index2"
:style="text.style"
:class="text.className"
>
{{ text.text }}
</span>
</p>
<div v-if="part.component">
<component :is="part.component" />
</div>
<div v-if="part.input" class="input-row">
<label>{{ part.input.label }}</label>
<input
v-if="inputs.get(part.input.key) !== undefined"
:type="part.input.type"
:ref="
(el) => {
if (part.input?.autofocus) autofocusElement = el;
}
"
@input="
(event: Event) => {
setInputValue(
props.contentKey,
part.input?.key,
(event.target as HTMLInputElement).value,
);
}
"
/>
</div>
</div>
</div>
</template>

<style lang="less" scoped>
.agdb-content {
p {
margin-bottom: 1rem;
}
}
.input-row {
display: flex;
gap: 1rem;
}
</style>
48 changes: 38 additions & 10 deletions agdb_studio/src/components/base/menu/AgdbMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,31 +13,44 @@ const openSubmenu = (key: string) => {
</script>

<template>
<div class="agdb-menu" @mouseleave="openedSubmenu = undefined">
<div
<ul class="agdb-menu" @mouseleave="openedSubmenu = undefined">
<li
v-for="action in props.actions"
:key="action.key"
@click="(event: MouseEvent) => action.action({ event })"
@click.prevent="
(event: MouseEvent) => {
if (action.actions) {
openSubmenu(action.key);
}
action.action({ event });
}
"
class="menu-item"
@mouseover="openSubmenu(action.key)"
:data-key="action.key"
>
{{ action.label }}
<span v-if="action.actions" class="menu-item-button">
<AkChevronRightSmall />
</span>
<a
href="#"
:class="{
active: openedSubmenu === action.key && action.actions,
}"
>
{{ action.label }}
<span v-if="action.actions" class="menu-item-button">
<AkChevronRightSmall />
</span>
</a>
<AgdbMenu
class="sub-menu"
v-if="openedSubmenu === action.key && action.actions"
:actions="action.actions"
/>
</div>
</div>
</li>
</ul>
</template>

<style lang="less" scoped>
.menu-item {
padding: 0.5rem;
cursor: pointer;
transition:
background-color 0.2s,
Expand All @@ -55,6 +68,21 @@ const openSubmenu = (key: string) => {
border-bottom-left-radius: 0.5rem;
border-bottom-right-radius: 0.5rem;
}
a {
padding: 0.5rem;
display: block;
color: var(--color-text);
text-decoration: none;
opacity: none;
transition: color 0.2s;
width: 100%;
height: 100%;
&:hover,
&.active {
color: var(--black);
}
}
}
.menu-item-button {
float: right;
Expand Down
54 changes: 52 additions & 2 deletions agdb_studio/src/components/base/modal/AgdbModal.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@ import { describe, beforeEach, it, expect } from "vitest";
import useModal from "@/composables/modal/modal";
import AgdbModal from "./AgdbModal.vue";
import { mount } from "@vue/test-utils";
import { convertArrayOfStringsToContent } from "@/utils/content";
import { convertArrayOfStringsToContent } from "@/composables/content/utils";

describe("AgdbModal", () => {
const { showModal, hideModal } = useModal();
beforeEach(() => {
hideModal();
});
const wrapper = mount(AgdbModal);

it("shows a modal when called", async () => {
const wrapper = mount(AgdbModal, {
attachTo: document.body,
});
expect(wrapper.isVisible()).toBe(false);
showModal({
header: "Test Header",
Expand All @@ -21,6 +23,9 @@ describe("AgdbModal", () => {
expect(wrapper.isVisible()).toBe(true);
});
it("hides a modal when clicked on close button", async () => {
const wrapper = mount(AgdbModal, {
attachTo: document.body,
});
showModal({
header: "Test Header",
content: convertArrayOfStringsToContent(["Test Body"]),
Expand All @@ -31,6 +36,9 @@ describe("AgdbModal", () => {
expect(wrapper.isVisible()).toBe(false);
});
it("hides a modal when clicked on close button in heades", async () => {
const wrapper = mount(AgdbModal, {
attachTo: document.body,
});
showModal({
header: "Test Header",
content: convertArrayOfStringsToContent(["Test Body"]),
Expand All @@ -41,6 +49,9 @@ describe("AgdbModal", () => {
expect(wrapper.isVisible()).toBe(false);
});
it("shows a modal with custom buttons", async () => {
const wrapper = mount(AgdbModal, {
attachTo: document.body,
});
showModal({
header: "Test Header",
content: convertArrayOfStringsToContent(["Test Body"]),
Expand All @@ -56,4 +67,43 @@ describe("AgdbModal", () => {
expect(wrapper.findAll(".button")).toHaveLength(2);
expect(wrapper.find(".button").text()).toBe("Custom Button");
});
it("sets focus on the submit button", async () => {
const wrapper = mount(AgdbModal, {
attachTo: document.body,
});
showModal({
header: "Test Header",
content: convertArrayOfStringsToContent(["Test Body"]),
onConfirm: () => {},
});
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
expect(
wrapper.find(".button[type=submit]").element.matches(":focus"),
).toBe(true);
});
it("won't set focus on the submit button if content has input with autofocus", async () => {
const wrapper = mount(AgdbModal, {
attachTo: document.body,
});
showModal({
header: "Test Header",
content: [
{
input: {
key: "test",
label: "New name",
type: "text",
autofocus: true,
},
},
],
onConfirm: () => {},
});
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
expect(
wrapper.find(".button[type=submit]").element.matches(":focus"),
).toBe(false);
});
});
Loading

0 comments on commit 36e0e79

Please sign in to comment.