Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vue3 form components #639

Merged
merged 38 commits into from
May 4, 2023
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
4d9e7af
feat(WIP): create form components
sgfost Mar 31, 2023
636b53d
feat(WIP): add datepicker form field component
sgfost Mar 31, 2023
3f38ed3
refactor: be more dry about form field setup
sgfost Mar 31, 2023
c9a4847
feat(WIP): add tagger form component
sgfost Apr 3, 2023
2982e39
fix: correct query url, value types for form tagger
sgfost Apr 3, 2023
806e714
chore: move form components to correct dir
sgfost Apr 4, 2023
d02d6d0
feat(experimental): create 'form builder' API
sgfost Apr 5, 2023
8a5b536
refactor: remove FormContext, experimental form API
sgfost Apr 5, 2023
477b925
feat: replace codebase search component
sgfost Apr 6, 2023
96b03ec
feat: port over requests apis
sgfost Apr 6, 2023
5604488
chore: format with prettier
sgfost Apr 6, 2023
dffff15
fix: turn off eslint no-unused-vars
sgfost Apr 7, 2023
4a81ed0
refactor: add type param to generic useField
sgfost Apr 7, 2023
223d068
test: remove helloworld test component, add search component test
sgfost Apr 7, 2023
46c7d63
docs: add documentation for composables (forms/api)
sgfost Apr 12, 2023
d25f15a
style: add back eslint no-unused-vars
sgfost Apr 12, 2023
f773359
feat: give 3rd party form fields bootstrap/comses styling
sgfost Apr 12, 2023
6f0b082
feat: add clear button to multiselect tagger
sgfost Apr 12, 2023
72a02ad
refactor: clean up some form components
sgfost Apr 12, 2023
58173dd
refactor: renaming, use yup.InferType for form typing
sgfost Apr 13, 2023
3fe1b54
feat: add textarea form component
sgfost Apr 13, 2023
0940642
feat: automatically convert ISO dates in useAxios
sgfost Apr 13, 2023
8c936c0
feat: add form field loading placeholder
sgfost Apr 14, 2023
6342812
refactor: refine datepicker component
sgfost Apr 14, 2023
b0d6320
feat(WIP): add event edit form
sgfost Apr 14, 2023
f08f738
refactor: update form components with placeholder inject, required name
sgfost Apr 14, 2023
e0fe7ff
feat: add success/error handling to useAxios/events form
sgfost Apr 17, 2023
894f22c
feat: add better error handling
sgfost Apr 18, 2023
bda2f20
feat: add summarize button to event edit form
sgfost Apr 19, 2023
fc5dcc2
feat: add placeholder markdown component
sgfost Apr 19, 2023
3a5e826
feat: add text item (list entry) component
sgfost Apr 19, 2023
9eb8a7d
feat: add profile edit form
sgfost Apr 19, 2023
73d9e35
feat: add ROR lookup typeahead component
sgfost Apr 20, 2023
780f3a2
feat: add org items/multiple affiliations component
sgfost Apr 20, 2023
db8c099
refactor: form/field component renaming
sgfost May 4, 2023
2860522
fix: revert prop type sharing
sgfost May 4, 2023
338f8df
refactor: clean up axios composable
sgfost May 4, 2023
937e6e6
test: add unit tests for useAxios + util functions
sgfost May 4, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 2 additions & 4 deletions django/library/jinja2/library/codebases/list.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@
{% include "library/includes/archive_model_help.jinja" %}
{{ pagination_block }}
<div id='sortby'></div>
{# django vite test #}
<div id="app"></div>
{{ vite_asset('apps/helloworld.ts') }}
{% endblock %}

{% block content %}
Expand All @@ -36,9 +33,10 @@
{% endblock %}

{% block sidebar %}
<div id="sidebar"></div>
<div id="search"></div>
{% endblock %}

{% block js %}
{{ render_bundle('codebase_list', attrs='defer') }}
{{ vite_asset('apps/codebase_list.ts') }}
{% endblock %}
15 changes: 0 additions & 15 deletions e2e/cypress/e2e/helloworld.spec.ts

This file was deleted.

6 changes: 6 additions & 0 deletions frontend-vue3/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ module.exports = {
'@vue/eslint-config-typescript',
'@vue/eslint-config-prettier/skip-formatting'
],
rules: {
'@typescript-eslint/no-unused-vars': [
'warn',
{ 'varsIgnorePattern': "props" }
],
},
parserOptions: {
ecmaVersion: 'latest'
}
Expand Down
10 changes: 9 additions & 1 deletion frontend-vue3/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,25 @@
"dependencies": {
"@fortawesome/fontawesome-free": "^5.12.0",
"@popperjs/core": "^2.11.6",
"@vorms/core": "^1.1.0",
"@vorms/resolvers": "^1.1.0",
"@vuepic/vue-datepicker": "^4.2.3",
"@vueuse/core": "^9.13.0",
"axios": "^1.3.5",
"bootstrap": "5.0.2",
"pinia": "^2.0.32",
"query-string": "^8.1.0",
"sass": "^1.58.3",
"vue": "^3.2.47",
"vue-multiselect": "^3.0.0-beta.1",
"vue-router": "^4.1.6"
"vue-router": "^4.1.6",
"yup": "^1.0.2"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.2.0",
"@types/jsdom": "^21.1.0",
"@types/node": "^18.14.2",
"@types/yup": "^0.32.0",
"@vitejs/plugin-vue": "^4.0.0",
"@vue/eslint-config-prettier": "^7.1.0",
"@vue/eslint-config-typescript": "^11.0.2",
Expand Down
6 changes: 6 additions & 0 deletions frontend-vue3/src/apps/codebase_list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import "vite/modulepreload-polyfill";

import { createApp } from "vue";
import CodebaseListSidebar from "@/components/CodebaseListSidebar.vue";

createApp(CodebaseListSidebar).mount("#search");
6 changes: 6 additions & 0 deletions frontend-vue3/src/apps/formdemo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import "vite/modulepreload-polyfill";

import { createApp } from "vue";
import FormDemo from "@/components/FormDemo.vue";

createApp(FormDemo).mount("#app");
4 changes: 2 additions & 2 deletions frontend-vue3/src/apps/helloworld.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import "vite/modulepreload-polyfill";

import { createApp } from "vue";
import HelloWorld from "@/components/HelloWorld.vue";
import FormDemo from "@/components/FormDemo.vue";

createApp(HelloWorld).mount("#app");
createApp(FormDemo).mount("#app");
74 changes: 74 additions & 0 deletions frontend-vue3/src/components/CodebaseListSidebar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<template>
<ListSidebar
create-label="Archive a model"
create-url="/codebases/add/"
search-label="Search"
:search-url="query"
>
<template #form>
<form @submit="handleSubmit">
<FormTextInput class="mb-3" name="keywords" label="Keywords" @keyup.enter="search" />
<FormDatePicker class="mb-3" name="startDate" label="Published After" string />
<FormDatePicker class="mb-3" name="endDate" label="Published Before" string />
<FormTagger class="mb-3" name="tags" label="Tags" type="Codebase" />
<FormSelect
class="mb-3"
name="peerReviewStatus"
label="Peer Review Status"
:options="peerReviewOptions"
/>
</form>
</template>
</ListSidebar>
</template>

<script setup lang="ts">
import * as yup from "yup";
import { computed } from "vue";
import ListSidebar from "@/components/ListSidebar.vue";
import FormTextInput from "@/components/form/FormTextInput.vue";
import FormSelect from "@/components/form/FormSelect.vue";
import FormDatePicker from "@/components/form/FormDatePicker.vue";
import FormTagger from "@/components/form/FormTagger.vue";
import { useForm } from "@/composables/form";
import { useCodebaseAPI } from "@/composables/api/codebase";

const peerReviewOptions = [
{ value: "reviewed", label: "Reviewed" },
{ value: "not_reviewed", label: "Not Reviewed" },
{ value: "", label: "Any" },
];

const schema = yup.object({
keywords: yup.string(),
startDate: yup.string(),
endDate: yup.string(),
tags: yup.array().of(yup.object().shape({ name: yup.string().required() })),
peerReviewStatus: yup.string(),
});
type SearchFields = yup.InferType<typeof schema>;

const { handleSubmit, values } = useForm<SearchFields>({
schema,
initialValues: { peerReviewStatus: "" },
onSubmit: () => {
window.location.href = query.value;
},
});

const { searchUrl } = useCodebaseAPI();

const query = computed(() => {
return searchUrl({
query: values.keywords,
published_after: values.startDate,
published_before: values.endDate,
tags: values.tags?.map(tag => tag.name),
peer_review_status: values.peerReviewStatus,
});
});

function search() {
window.location.href = query.value;
}
</script>
46 changes: 46 additions & 0 deletions frontend-vue3/src/components/FormDemo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<template>
<form @submit="handleSubmit">
<FormTextInput class="mb-3" name="text" label="Text Input" />
<FormTextInput class="mb-3" name="email" label="Email" />
<FormCheckbox class="mb-3" name="terms" label="Terms and Conditions" />
<FormSelect class="mb-3" name="number" label="Your Number" :options="numberOptions" />
<FormDatePicker class="mb-3" name="date" label="Date" />
<FormTagger class="mb-3" name="tags" label="Tags" />
<button type="submit" class="mb-3 btn btn-primary">Submit</button>
</form>
</template>

<script setup lang="ts">
import * as yup from "yup";
import { useForm } from "@/composables/form";
import FormTextInput from "@/components/form/FormTextInput.vue";
import FormSelect from "@/components/form/FormSelect.vue";
import FormCheckbox from "@/components/form/FormCheckbox.vue";
import FormDatePicker from "@/components/form/FormDatePicker.vue";
import FormTagger from "@/components/form/FormTagger.vue";

const numberOptions = [
{ value: 1, label: "One" },
{ value: 2, label: "Two" },
{ value: 3, label: "Three" },
{ value: 4, label: "Four" },
];

const schema = yup.object({
text: yup.string().required(),
email: yup.string().email(),
terms: yup.boolean().oneOf([true], "You must accept"),
date: yup.date(),
number: yup.number().required().oneOf([2, 3], "Must be 2 or 3").label("Number"),
tags: yup.array().of(yup.object().shape({ name: yup.string().required() })),
});
type DemoFields = yup.InferType<typeof schema>;

const { handleSubmit } = useForm<DemoFields>({
initialValues: { terms: false },
schema,
onSubmit: values => {
console.log(values);
},
});
</script>
17 changes: 0 additions & 17 deletions frontend-vue3/src/components/HelloWorld.vue

This file was deleted.

30 changes: 30 additions & 0 deletions frontend-vue3/src/components/ListSidebar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<template>
<div>
<a v-if="createUrl && createLabel" class="text-white" :href="createUrl">
<div class="btn btn-secondary w-100" tabindex="0">
{{ createLabel }}
</div>
</a>
<div class="card-metadata">
<h2 class="title">Search</h2>
<div class="card-body">
<slot name="form" />
</div>
</div>
<a class="text-white" :href="searchUrl">
<div class="btn btn-primary w-100" tabindex="0">
{{ searchLabel }}
</div>
</a>
</div>
</template>
<script setup lang="ts">
export interface SearchProps {
createLabel?: string;
createUrl?: string;
searchLabel: string;
searchUrl: string;
}

const props = defineProps<SearchProps>();
</script>
42 changes: 42 additions & 0 deletions frontend-vue3/src/components/__tests__/CodebaseSearch.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { mount } from "@vue/test-utils";
import CodebaseListSidebar from "@/components/CodebaseListSidebar.vue";
import ListSidebar from "@/components/ListSidebar.vue";
import FormTextInput from "@/components/form/FormTextInput.vue";
import FormDatePicker from "@/components/form/FormDatePicker.vue";
import FormTagger from "@/components/form/FormTagger.vue";
import FormSelect from "@/components/form/FormSelect.vue";

describe("CodebaseListSidebar.vue", () => {
it("renders the form field components", async () => {
const wrapper = mount(CodebaseListSidebar);

const baseSearch = wrapper.findComponent(ListSidebar);
expect(baseSearch.exists()).toBe(true);

const formTextInput = wrapper.findComponent(FormTextInput);
expect(formTextInput.exists()).toBe(true);

const anyDatePicker = wrapper.findComponent(FormDatePicker);
expect(anyDatePicker.exists()).toBe(true);

const formTagger = wrapper.findComponent(FormTagger);
expect(formTagger.exists()).toBe(true);

const formSelect = wrapper.findComponent(FormSelect);
expect(formSelect.exists()).toBe(true);
expect(formSelect.props("options").length).toEqual(3);
});

it("updates the query computed value based on form inputs", async () => {
const wrapper = mount(CodebaseListSidebar);

const formTextInput = wrapper.findComponent(FormTextInput);
const formTextInputElement = formTextInput.find("input");
formTextInputElement.element.value = "test keyword";
await formTextInputElement.trigger("input");
await wrapper.vm.$nextTick();
const baseSearch = wrapper.findComponent(ListSidebar);
const searchUrl = baseSearch.props("searchUrl");
expect(searchUrl).toContain("query=test%20keyword");
});
});
19 changes: 0 additions & 19 deletions frontend-vue3/src/components/__tests__/HelloWorld.test.ts

This file was deleted.

41 changes: 41 additions & 0 deletions frontend-vue3/src/components/form/FormCheckbox.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<template>
<div>
<div class="form-check">
<input
v-model="value"
type="checkbox"
:id="id"
v-bind="attrs"
:class="{ 'form-check-input': true, 'is-invalid': error }"
/>
<FormLabel v-if="label" :label="label" :id-for="id" :required="required" />
</div>
<slot name="help">
<FormHelp v-if="help" :help="help" :id-for="id" />
</slot>
<slot name="error">
<FormError v-if="error" :error="error" :id-for="id" />
</slot>
</div>
</template>

<script setup lang="ts">
import { useField } from "@/composables/form";
import FormLabel from "@/components/form/FormLabel.vue";
import FormHelp from "@/components/form/FormHelp.vue";
import FormError from "@/components/form/FormError.vue";

export interface CheckboxProps {
name: string;
label?: string;
help?: string;
placeholder?: string;
required?: boolean;
}

const props = withDefaults(defineProps<CheckboxProps>(), {
help: "",
});

const { id, value, attrs, error } = useField<boolean>(props, "name");
</script>