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 1 commit
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion django/library/jinja2/library/codebases/list.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,5 @@

{% block js %}
{{ render_bundle('codebase_list', attrs='defer') }}
{{ vite_asset('apps/codebaselist.ts') }}
{{ vite_asset('apps/codebase_list.ts') }}
{% endblock %}
4 changes: 2 additions & 2 deletions frontend-vue3/src/components/CodebaseListSidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
<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" />
<FormDatePicker class="mb-3" name="endDate" label="Published Before" />
<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"
Expand Down
14 changes: 7 additions & 7 deletions frontend-vue3/src/components/__tests__/CodebaseSearch.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { mount } from "@vue/test-utils";
import CodebaseSearch from "@/components/CodebaseSearch.vue";
import BaseSearch from "@/components/BaseSearch.vue";
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("CodebaseSearch.vue", () => {
describe("CodebaseListSidebar.vue", () => {
it("renders the form field components", async () => {
const wrapper = mount(CodebaseSearch);
const wrapper = mount(CodebaseListSidebar);

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

const formTextInput = wrapper.findComponent(FormTextInput);
Expand All @@ -28,14 +28,14 @@ describe("CodebaseSearch.vue", () => {
});

it("updates the query computed value based on form inputs", async () => {
const wrapper = mount(CodebaseSearch);
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(BaseSearch);
const baseSearch = wrapper.findComponent(ListSidebar);
const searchUrl = baseSearch.props("searchUrl");
expect(searchUrl).toContain("query=test%20keyword");
});
Expand Down
12 changes: 7 additions & 5 deletions frontend-vue3/src/components/form/FormDatePicker.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
</slot>
<VueDatePicker
v-model="value"
model-type="yyyy-MM-dd"
:model-type="string ? 'yyyy-MM-dd' : undefined"
format="yyyy-MM-dd"
:id="id"
v-bind="attrs"
:placeholder="placeholder"
:class="{ 'is-invalid': error }"
input-class-name="custom-dp__input"
menu-class-name="custom-dp__menu"
Expand All @@ -32,17 +33,18 @@ import FormLabel from "@/components/form/FormLabel.vue";
import FormHelp from "@/components/form/FormHelp.vue";
import FormError from "@/components/form/FormError.vue";

export type SelectOptions = { value: any; label: string }[];

export interface DatePickerProps {
name: string;
label?: string;
help?: string;
placeholder?: string;
required?: boolean;
string?: boolean; // whether to use string or date for the model value
alee marked this conversation as resolved.
Show resolved Hide resolved
}

const props = defineProps<DatePickerProps>();
const props = withDefaults(defineProps<DatePickerProps>(), {
string: false,
});

const { id, value, attrs, error } = useField<string>(props, "name");
const { id, value, attrs, error } = useField<Date | string>(props, "name");
</script>
38 changes: 35 additions & 3 deletions frontend-vue3/src/composables/api/axios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export function useAxios(baseUrl?: string, config?: AxiosRequestConfig) {
...config,
});

// add CSRF token to all requests
instance.interceptors.request.use(
config => {
const csrfToken = getCookie("csrftoken");
Expand All @@ -36,6 +37,17 @@ export function useAxios(baseUrl?: string, config?: AxiosRequestConfig) {
error => Promise.reject(error)
);

// convert date strings to Date objects in response data
instance.interceptors.response.use(
response => {
if (response.data) {
convertDates(response.data);
}
return response;
},
error => Promise.reject(error)
);

const state = reactive<AxiosRequestState>({
response: null,
data: undefined,
Expand Down Expand Up @@ -119,12 +131,32 @@ export function useAxios(baseUrl?: string, config?: AxiosRequestConfig) {
};
}

/**
* Utility functions
*/
function getCookie(name: string) {
/**
* utility function for getting a cookie's value by name
*/
return document.cookie.split("; ").reduce((r, v) => {
const [n, ...val] = v.split("=");
return n === name ? decodeURIComponent(val.join("=")) : r;
}, "");
}

const ISODateFormat = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d*)?(?:[-+]\d{2}:?\d{2}|Z)?$/;

function isISODateString(value: any) {
return value && typeof value === "string" && ISODateFormat.test(value);
}

function convertDates(body: any) {
if (body === null || body === undefined || typeof body !== "object") {
return body;
}
for (const key of Object.keys(body)) {
const value = body[key];
if (isISODateString(value)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could also use Date.parse, try/catch or are we only allowing iso date strings here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah the intent was to strictly parse ISO 8601 date strings and assume everything else is meant to be a string, the spec for Date.parse seemed far too loose since most implementations would parse a string like "10 10 10"

body[key] = new Date(value);
} else if (typeof value === "object") {
convertDates(value);
}
}
}
3 changes: 2 additions & 1 deletion frontend-vue3/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ export default defineConfig({
input: {
main: resolvePath("./src/apps/main.ts"),
formdemo: resolvePath("./src/apps/formdemo.ts"),
codebaselist: resolvePath("./src/apps/codebaselist.ts"),
codebase_list: resolvePath("./src/apps/codebase_list.ts"),
event_edit: resolvePath("./src/apps/event_edit.ts"),
// add more entry points here
},
},
Expand Down