more form changes
This commit is contained in:
parent
c76db68e5b
commit
b1f58276d0
60
package-lock.json
generated
60
package-lock.json
generated
@ -12,8 +12,10 @@
|
||||
"@heroicons/vue": "^2.0.13",
|
||||
"@vueuse/core": "^9.10.0",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.omit": "^4.5.0",
|
||||
"lodash.pick": "^4.4.0",
|
||||
"lodash.set": "^4.3.2",
|
||||
"pinia": "^2.0.28",
|
||||
"sass": "^1.57.1",
|
||||
"vue": "^3.2.45",
|
||||
@ -22,8 +24,10 @@
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"@tailwindcss/line-clamp": "^0.4.2",
|
||||
"@types/lodash.get": "^4.4.7",
|
||||
"@types/lodash.omit": "^4.5.7",
|
||||
"@types/lodash.pick": "^4.4.7",
|
||||
"@types/lodash.set": "^4.3.7",
|
||||
"@vitejs/plugin-vue": "^4.0.0",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"postcss": "^8.4.21",
|
||||
@ -479,6 +483,15 @@
|
||||
"integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/lodash.get": {
|
||||
"version": "4.4.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash.get/-/lodash.get-4.4.7.tgz",
|
||||
"integrity": "sha512-af34Mj+KdDeuzsJBxc/XeTtOx0SZHZNLd+hdrn+PcKGQs0EG2TJTzQAOTCZTgDJCArahlCzLWSy8c2w59JRz7Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/lodash.omit": {
|
||||
"version": "4.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash.omit/-/lodash.omit-4.5.7.tgz",
|
||||
@ -497,6 +510,15 @@
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/lodash.set": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash.set/-/lodash.set-4.3.7.tgz",
|
||||
"integrity": "sha512-bS5Wkg/nrT82YUfkNYPSccFrNZRL+irl7Yt4iM6OTSQ0VZJED2oUIVm15NkNtUAQ8SRhCe+axqERUV6MJgkeEg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/web-bluetooth": {
|
||||
"version": "0.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz",
|
||||
@ -1248,6 +1270,11 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash.get": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
|
||||
"integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ=="
|
||||
},
|
||||
"node_modules/lodash.omit": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz",
|
||||
@ -1258,6 +1285,11 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz",
|
||||
"integrity": "sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q=="
|
||||
},
|
||||
"node_modules/lodash.set": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz",
|
||||
"integrity": "sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg=="
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz",
|
||||
@ -2218,6 +2250,15 @@
|
||||
"integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/lodash.get": {
|
||||
"version": "4.4.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash.get/-/lodash.get-4.4.7.tgz",
|
||||
"integrity": "sha512-af34Mj+KdDeuzsJBxc/XeTtOx0SZHZNLd+hdrn+PcKGQs0EG2TJTzQAOTCZTgDJCArahlCzLWSy8c2w59JRz7Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
},
|
||||
"@types/lodash.omit": {
|
||||
"version": "4.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash.omit/-/lodash.omit-4.5.7.tgz",
|
||||
@ -2236,6 +2277,15 @@
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
},
|
||||
"@types/lodash.set": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash.set/-/lodash.set-4.3.7.tgz",
|
||||
"integrity": "sha512-bS5Wkg/nrT82YUfkNYPSccFrNZRL+irl7Yt4iM6OTSQ0VZJED2oUIVm15NkNtUAQ8SRhCe+axqERUV6MJgkeEg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
},
|
||||
"@types/web-bluetooth": {
|
||||
"version": "0.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz",
|
||||
@ -2786,6 +2836,11 @@
|
||||
"integrity": "sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==",
|
||||
"dev": true
|
||||
},
|
||||
"lodash.get": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
|
||||
"integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ=="
|
||||
},
|
||||
"lodash.omit": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz",
|
||||
@ -2796,6 +2851,11 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz",
|
||||
"integrity": "sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q=="
|
||||
},
|
||||
"lodash.set": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz",
|
||||
"integrity": "sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg=="
|
||||
},
|
||||
"magic-string": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz",
|
||||
|
@ -13,8 +13,10 @@
|
||||
"@heroicons/vue": "^2.0.13",
|
||||
"@vueuse/core": "^9.10.0",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.omit": "^4.5.0",
|
||||
"lodash.pick": "^4.4.0",
|
||||
"lodash.set": "^4.3.2",
|
||||
"pinia": "^2.0.28",
|
||||
"sass": "^1.57.1",
|
||||
"vue": "^3.2.45",
|
||||
@ -23,8 +25,10 @@
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"@tailwindcss/line-clamp": "^0.4.2",
|
||||
"@types/lodash.get": "^4.4.7",
|
||||
"@types/lodash.omit": "^4.5.7",
|
||||
"@types/lodash.pick": "^4.4.7",
|
||||
"@types/lodash.set": "^4.3.7",
|
||||
"@vitejs/plugin-vue": "^4.0.0",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"postcss": "^8.4.21",
|
||||
|
52
src/components/Accordion.vue
Normal file
52
src/components/Accordion.vue
Normal file
@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<button class="px-4 py-4" @click="open = !open" :aria-expanded="open">
|
||||
<slot name="title" :open="open" :close="close">
|
||||
<div class="flex items-center justify-between">
|
||||
<span>{{ title }}</span>
|
||||
|
||||
<ChevronDownIcon class="h-6 w-6" v-if="!open" aria-hidden="true" />
|
||||
<ChevronUpIcon class="h-6 w-6" v-else aria-hidden="true" />
|
||||
</div>
|
||||
</slot>
|
||||
</button>
|
||||
|
||||
<Transition
|
||||
enter-active-class="transition-max-height ease-out duration-500"
|
||||
enter-from-class="max-h-0"
|
||||
enter-to-class="max-h-screen"
|
||||
leave-active-class="transition-max-height ease-in duration-250"
|
||||
leave-from-class="max-h-screen"
|
||||
leave-to-class="max-h-0"
|
||||
>
|
||||
<div
|
||||
v-if="open"
|
||||
:class="[
|
||||
open ? 'border-t-2 border-gray-100' : '',
|
||||
'overflow-hidden px-4 py-4',
|
||||
]"
|
||||
>
|
||||
<slot :close="close" />
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/vue/24/outline';
|
||||
import { ref } from 'vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
title?: string;
|
||||
open?: boolean;
|
||||
}>(),
|
||||
{
|
||||
open: false,
|
||||
}
|
||||
);
|
||||
|
||||
const open = ref(props.open);
|
||||
|
||||
const close = () => (open.value = false);
|
||||
</script>
|
@ -23,19 +23,18 @@
|
||||
:onBlur="onBlur"
|
||||
:setValue="setValue"
|
||||
>
|
||||
<FormInput
|
||||
:for-id="forId"
|
||||
<input
|
||||
:type="type"
|
||||
:id="forId"
|
||||
:name="name"
|
||||
:value="value"
|
||||
:class="inputClass"
|
||||
:placeholder="placeholder"
|
||||
:invalid="!!errors?.length"
|
||||
:disabled="disabled"
|
||||
@input="onInput"
|
||||
@change="onChange"
|
||||
@focus="onFocus"
|
||||
@blur="onBlur"
|
||||
@set-value="setValue"
|
||||
/>
|
||||
</slot>
|
||||
<slot />
|
||||
@ -54,7 +53,6 @@ import get from 'lodash.get';
|
||||
import { computed, ref, Ref } from 'vue';
|
||||
import { inject } from 'vue';
|
||||
import { FormData, FormErrors } from './form.types';
|
||||
import FormInput from './FormInput.vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@ -68,10 +66,12 @@ const props = withDefaults(
|
||||
| 'radio';
|
||||
label: string;
|
||||
name: string;
|
||||
disabled?: boolean;
|
||||
placeholder?: string;
|
||||
}>(),
|
||||
{
|
||||
type: 'text',
|
||||
disabled: false,
|
||||
}
|
||||
);
|
||||
|
||||
@ -90,7 +90,7 @@ const fieldName = computed(() =>
|
||||
formGroup?.value ? `${formGroup.value}.${props.name}` : props.name
|
||||
);
|
||||
|
||||
const forId = computed(() => `form-${fieldName}`);
|
||||
const forId = computed(() => `form-${fieldName.value}`);
|
||||
const value = computed(() => get(formData?.value, fieldName.value));
|
||||
const errors = computed(() => formErrors?.value[fieldName.value] || []);
|
||||
|
||||
@ -122,4 +122,13 @@ const onBlur = () => {
|
||||
emit('blur');
|
||||
fieldChange?.(fieldName.value);
|
||||
};
|
||||
|
||||
const inputClass = computed(() => {
|
||||
return [
|
||||
errors.value.length
|
||||
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
|
||||
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500',
|
||||
`mt-1 block w-full rounded-md shadow-sm sm:text-sm transition-colors duration-200`,
|
||||
];
|
||||
});
|
||||
</script>
|
||||
|
@ -1,65 +0,0 @@
|
||||
<template>
|
||||
<ColorInput
|
||||
v-if="type === 'color'"
|
||||
:for-id="forId"
|
||||
:model-value="(value as string)"
|
||||
@update:model-value="(newValue: string) => emit('setValue', newValue)"
|
||||
/>
|
||||
<input
|
||||
v-else
|
||||
:type="type"
|
||||
:id="forId"
|
||||
:name="name"
|
||||
:value="value"
|
||||
:class="inputClass"
|
||||
:placeholder="placeholder"
|
||||
@input="emit('input', $event)"
|
||||
@change="emit('change', $event)"
|
||||
@focus="emit('focus', $event)"
|
||||
@blur="emit('blur', $event)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import ColorInput from '../ColorInput.vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
type?:
|
||||
| 'text'
|
||||
| 'number'
|
||||
| 'password'
|
||||
| 'color'
|
||||
| 'email'
|
||||
| 'checkbox'
|
||||
| 'radio';
|
||||
forId: string;
|
||||
name: string;
|
||||
value: unknown;
|
||||
invalid?: boolean;
|
||||
placeholder?: string;
|
||||
}>(),
|
||||
{
|
||||
type: 'text',
|
||||
invalid: false,
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'setValue', value: unknown): void;
|
||||
(e: 'input', ev: Event): void;
|
||||
(e: 'change', ev: Event): void;
|
||||
(e: 'focus', ev: Event): void;
|
||||
(e: 'blur', ev: Event): void;
|
||||
}>();
|
||||
|
||||
const inputClass = computed(() => {
|
||||
return [
|
||||
props.invalid
|
||||
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
|
||||
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500',
|
||||
`mt-1 block w-full rounded-md shadow-sm sm:text-sm transition-colors duration-200`,
|
||||
];
|
||||
});
|
||||
</script>
|
130
src/components/form/base/Select.vue
Normal file
130
src/components/form/base/Select.vue
Normal file
@ -0,0 +1,130 @@
|
||||
<template>
|
||||
<Listbox
|
||||
:modelValue="selected"
|
||||
@update:modelValue="valueChanged"
|
||||
:disabled="disabled"
|
||||
by="value"
|
||||
>
|
||||
<div class="relative mt-1">
|
||||
<ListboxButton :class="inputClass" :id="forId">
|
||||
<span class="block truncate">{{ selected?.name || '' }}</span>
|
||||
<span
|
||||
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
|
||||
>
|
||||
<ChevronUpDownIcon class="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||
</span>
|
||||
</ListboxButton>
|
||||
|
||||
<Transition
|
||||
leave-active-class="transition duration-100 ease-in"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<ListboxOptions
|
||||
class="absolute z-20 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
|
||||
>
|
||||
<ListboxOption
|
||||
v-slot="{ active, selected }"
|
||||
v-for="option in options"
|
||||
:key="option.name"
|
||||
:value="option"
|
||||
as="template"
|
||||
>
|
||||
<li :class="itemClass(active, selected)">
|
||||
<span
|
||||
:class="[
|
||||
selected ? 'font-medium' : 'font-normal',
|
||||
'block truncate',
|
||||
]"
|
||||
>
|
||||
{{ option.name }}
|
||||
</span>
|
||||
<span
|
||||
v-if="selected"
|
||||
class="absolute inset-y-0 left-0 flex items-center pl-2 text-blue-600"
|
||||
>
|
||||
<CheckIcon class="h-5 w-5" aria-hidden="true" />
|
||||
</span>
|
||||
</li>
|
||||
</ListboxOption>
|
||||
</ListboxOptions>
|
||||
</Transition>
|
||||
</div>
|
||||
</Listbox>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import {
|
||||
Listbox,
|
||||
ListboxButton,
|
||||
ListboxOptions,
|
||||
ListboxOption,
|
||||
} from '@headlessui/vue';
|
||||
import { CheckIcon, ChevronUpDownIcon } from '@heroicons/vue/24/outline';
|
||||
|
||||
export interface SelectOption {
|
||||
value: string | number;
|
||||
name: string;
|
||||
description?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
forId?: string;
|
||||
modelValue: string | number | null;
|
||||
options: SelectOption[];
|
||||
disabled?: boolean;
|
||||
invalid?: boolean;
|
||||
placeholder?: string;
|
||||
}>(),
|
||||
{
|
||||
modelValue: null,
|
||||
disabled: false,
|
||||
invalid: false,
|
||||
}
|
||||
);
|
||||
|
||||
const localValue = ref(props.modelValue);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: string | number | null): void;
|
||||
}>();
|
||||
|
||||
const valueChanged = (option: SelectOption) => {
|
||||
localValue.value = option.value;
|
||||
emit('update:modelValue', localValue.value);
|
||||
};
|
||||
|
||||
const selected = computed(() =>
|
||||
props.options.find((item) => localValue.value === item.value)
|
||||
);
|
||||
|
||||
const inputClass = computed(() => {
|
||||
return [
|
||||
props.invalid
|
||||
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
|
||||
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500',
|
||||
`block w-full rounded-md shadow-sm sm:text-sm transition-colors duration-200`,
|
||||
'py-2 pl-3 pr-10 text-left cursor-default h-[38px] border-[1px]',
|
||||
];
|
||||
});
|
||||
|
||||
const itemClass = (active: boolean, selected: boolean) => {
|
||||
const classList = ['relative cursor-default select-none py-2 pl-8 pr-4'];
|
||||
if (selected) {
|
||||
classList.push('text-blue-900');
|
||||
}
|
||||
|
||||
if (active && selected) {
|
||||
classList.push('bg-blue-200');
|
||||
} else if (active || selected) {
|
||||
classList.push('bg-blue-100');
|
||||
} else {
|
||||
classList.push('text-gray-900');
|
||||
}
|
||||
|
||||
return classList;
|
||||
};
|
||||
</script>
|
35
src/components/form/fields/FormColorField.vue
Normal file
35
src/components/form/fields/FormColorField.vue
Normal file
@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<FormField
|
||||
:name="name"
|
||||
:label="label"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<template #input="{ forId, value, setValue }">
|
||||
<ColorInput
|
||||
:for-id="forId"
|
||||
:model-value="(value as string)"
|
||||
@update:model-value="setValue"
|
||||
/>
|
||||
</template>
|
||||
<template #default><slot /></template>
|
||||
</FormField>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import FormField from '../FormField.vue';
|
||||
import ColorInput from '../base/ColorInput.vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
forId?: string;
|
||||
label: string;
|
||||
name: string;
|
||||
disabled?: boolean;
|
||||
placeholder?: string;
|
||||
}>(),
|
||||
{
|
||||
disabled: false,
|
||||
}
|
||||
);
|
||||
</script>
|
40
src/components/form/fields/FormSelectField.vue
Normal file
40
src/components/form/fields/FormSelectField.vue
Normal file
@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<FormField
|
||||
:name="name"
|
||||
:label="label"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<template #input="{ invalid, value, setValue }">
|
||||
<SelectVue
|
||||
:for-id="forId"
|
||||
:invalid="invalid"
|
||||
:disabled="disabled"
|
||||
:options="options || []"
|
||||
:placeholder="placeholder"
|
||||
:model-value="(value as string)"
|
||||
@update:model-value="(newValue) => setValue(newValue)"
|
||||
/>
|
||||
</template>
|
||||
<template #default><slot /></template>
|
||||
</FormField>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import FormField from '../FormField.vue';
|
||||
import SelectVue, { SelectOption } from '../base/Select.vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
forId?: string;
|
||||
label: string;
|
||||
name: string;
|
||||
options: SelectOption[];
|
||||
disabled?: boolean;
|
||||
placeholder?: string;
|
||||
}>(),
|
||||
{
|
||||
disabled: false,
|
||||
}
|
||||
);
|
||||
</script>
|
@ -1,41 +0,0 @@
|
||||
<template>
|
||||
<Dropdown :title="title">
|
||||
<template #trigger="{ title, open, toggle }">
|
||||
<button
|
||||
type="button"
|
||||
@click="() => toggle()"
|
||||
:aria-expanded="open"
|
||||
:class="[
|
||||
open ? 'text-gray-900' : 'text-gray-500',
|
||||
'group inline-flex items-center rounded-md bg-white text-base font-medium hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2',
|
||||
]"
|
||||
>
|
||||
<span>{{ title }}</span>
|
||||
<ChevronDownIcon class="ml-2 h-5 w-5" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<template #default="{ title, open, toggle }">
|
||||
<div
|
||||
class="absolute z-10 -ml-4 mt-3 w-screen max-w-xs transform px-2 sm:px-0 lg:left-1/2 lg:ml-0 lg:-translate-x-1/2"
|
||||
>
|
||||
<div
|
||||
class="overflow-hidden rounded-lg shadow-lg ring-1 ring-black ring-opacity-5"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ChevronDownIcon } from '@heroicons/vue/24/outline';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { onBeforeRouteLeave } from 'vue-router';
|
||||
import Dropdown from '../Dropdown.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
title: string;
|
||||
}>();
|
||||
</script>
|
@ -1,65 +1,143 @@
|
||||
<template>
|
||||
<div class="relative border-b-2 border-gray-100 bg-white">
|
||||
<div class="mx-auto max-w-7xl px-5">
|
||||
<div
|
||||
class="flex items-center justify-between py-5 md:justify-start md:space-x-10"
|
||||
>
|
||||
<div class="flex w-0 justify-start md:mr-4">
|
||||
<router-link to="/"><HomeIcon class="h-8 w-8" /></router-link>
|
||||
<Popover class="relative border-b-2 border-gray-100 bg-white">
|
||||
<div class="mx-auto max-w-7xl px-6">
|
||||
<div class="flex items-center py-6 md:justify-start md:space-x-8">
|
||||
<div class="mr-auto flex md:mr-0">
|
||||
<div class="mr-auto flex justify-start md:mr-0">
|
||||
<router-link to="/"><HomeIcon class="h-8 w-8" /></router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="-my-2 -mr-2 md:hidden">
|
||||
<button
|
||||
type="button"
|
||||
<PopoverButton
|
||||
class="inline-flex items-center justify-center rounded-md bg-white p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<span class="sr-only">Open menu</span>
|
||||
<Bars3Icon class="h-6 w-6" />
|
||||
</button>
|
||||
<Bars3Icon class="h-6 w-6" aria-hidden="true" />
|
||||
</PopoverButton>
|
||||
</div>
|
||||
<PopoverGroup as="nav" class="hidden space-x-6 md:flex">
|
||||
<Popover class="relative" v-slot="{ open }">
|
||||
<PopoverButton
|
||||
:class="[
|
||||
open ? 'text-gray-900' : 'text-gray-500',
|
||||
'group inline-flex items-center rounded-md bg-white text-base font-medium hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2',
|
||||
]"
|
||||
>
|
||||
<span>Buildings</span>
|
||||
<ChevronDownIcon
|
||||
:class="[
|
||||
open ? 'text-gray-600' : 'text-gray-400',
|
||||
'ml-2 h-5 w-5 group-hover:text-gray-500',
|
||||
]"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</PopoverButton>
|
||||
|
||||
<nav class="hidden space-x-10 md:flex">
|
||||
<Dropdown title="My Buildings">
|
||||
<div class="relative bg-white pt-4 pb-4 sm:gap-5">
|
||||
<template v-for="building of buildings">
|
||||
<Building :building="building" />
|
||||
</template>
|
||||
</div>
|
||||
</Dropdown>
|
||||
<button
|
||||
type="button"
|
||||
:class="[
|
||||
'text-gray-500',
|
||||
'inline-flex items-center rounded-md bg-white text-base font-medium hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2',
|
||||
]"
|
||||
<transition
|
||||
enter-active-class="transition ease-out duration-200"
|
||||
enter-from-class="opacity-0 translate-y-1"
|
||||
enter-to-class="opacity-100 translate-y-0"
|
||||
leave-active-class="transition ease-in duration-150"
|
||||
leave-from-class="opacity-100 translate-y-0"
|
||||
leave-to-class="opacity-0 translate-y-1"
|
||||
>
|
||||
<PopoverPanel
|
||||
class="absolute z-10 -ml-4 mt-3 w-screen max-w-md transform px-2 sm:px-0"
|
||||
>
|
||||
<div
|
||||
class="overflow-hidden rounded-lg shadow-lg ring-1 ring-black ring-opacity-5"
|
||||
>
|
||||
<div class="relative bg-white pt-4 pb-4 sm:gap-5">
|
||||
<template v-for="building of buildings">
|
||||
<Building :building="building" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverPanel>
|
||||
</transition>
|
||||
</Popover>
|
||||
|
||||
<a
|
||||
href="#"
|
||||
class="text-base font-medium text-gray-500 hover:text-gray-900"
|
||||
>Groups</a
|
||||
>
|
||||
<span>Groups</span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
</PopoverGroup>
|
||||
<div class="hidden items-center justify-end md:flex md:flex-1 lg:w-0">
|
||||
<UserPill :user="user" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<transition
|
||||
enter-active-class="duration-200 ease-out"
|
||||
enter-from-class="opacity-0 scale-95"
|
||||
enter-to-class="opacity-100 scale-100"
|
||||
leave-active-class="duration-100 ease-in"
|
||||
leave-from-class="opacity-100 scale-100"
|
||||
leave-to-class="opacity-0 scale-95"
|
||||
>
|
||||
<PopoverPanel
|
||||
focus
|
||||
class="absolute inset-x-0 top-0 z-50 origin-top-right transform p-2 transition md:hidden"
|
||||
>
|
||||
<div
|
||||
class="divide-y-2 divide-gray-50 rounded-lg bg-white shadow-lg ring-1 ring-black ring-opacity-5"
|
||||
>
|
||||
<div class="px-5 pt-5 pb-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<router-link to="/"><HomeIcon class="h-8 w-8" /></router-link>
|
||||
</div>
|
||||
<div class="-mr-2">
|
||||
<PopoverButton
|
||||
class="inline-flex items-center justify-center rounded-md bg-white p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500"
|
||||
>
|
||||
<span class="sr-only">Close menu</span>
|
||||
<XMarkIcon class="h-6 w-6" aria-hidden="true" />
|
||||
</PopoverButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
<span class="text-lg font-bold">Buildings</span>
|
||||
<div class="-mx-5">
|
||||
<template v-for="building of buildings">
|
||||
<Building :building="building" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-6 py-6 px-5">
|
||||
<div class="flex justify-center">
|
||||
<UserPill :user="user" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverPanel>
|
||||
</transition>
|
||||
</Popover>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Popover,
|
||||
PopoverButton,
|
||||
PopoverGroup,
|
||||
PopoverPanel,
|
||||
} from '@headlessui/vue';
|
||||
import {
|
||||
HomeIcon,
|
||||
Bars3Icon,
|
||||
ChevronDownIcon,
|
||||
UserIcon,
|
||||
XMarkIcon,
|
||||
} from '@heroicons/vue/24/outline';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useUserStore } from '../../store/user.store';
|
||||
import UserPill from './UserPill.vue';
|
||||
import Dropdown from './Dropdown.vue';
|
||||
import Building from './Building.vue';
|
||||
import { useBuildingStore } from '../../store/building.store';
|
||||
import { onMounted } from 'vue';
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
const userStore = useUserStore();
|
||||
const buildingsStore = useBuildingStore();
|
||||
const { user } = storeToRefs(userStore);
|
||||
|
@ -50,7 +50,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from '@vue/reactivity';
|
||||
import ColorInput from '../ColorInput.vue';
|
||||
import ColorInput from '../form/base/ColorInput.vue';
|
||||
import { ObjectProperty } from './interfaces/properties.interfaces';
|
||||
|
||||
const props = defineProps<{
|
||||
|
@ -8,11 +8,11 @@
|
||||
<FormAlert :message="error" />
|
||||
<Form @submit="onSubmit" v-model="data" :validators="validators">
|
||||
<FormField name="displayName" label="Display Name" />
|
||||
<FormField name="type" label="Type" />
|
||||
<FormSelectField :options="selectOptions" name="type" label="Type" />
|
||||
<FormField name="locationDescription" label="Location description">
|
||||
<span class="text-sm">Describe the location of this storage</span>
|
||||
</FormField>
|
||||
<FormField type="color" name="color" label="Color" />
|
||||
<FormColorField name="color" label="Color" />
|
||||
<button type="submit">Submit</button>
|
||||
</Form>
|
||||
</template>
|
||||
@ -20,9 +20,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useAccessToken } from '../../composables/useAccessToken';
|
||||
import { BACKEND_URL } from '../../constants';
|
||||
import { StorageSetTypeName } from '../../enums/storage-set-type.enum';
|
||||
import { StorageTypeName } from '../../enums/storage-type.enum';
|
||||
import { BuildingListItem } from '../../interfaces/building.interfaces';
|
||||
import { RoomListItem } from '../../interfaces/room.interfaces';
|
||||
import {
|
||||
@ -35,8 +37,11 @@ import { FormSubmit } from '../form/form.types';
|
||||
import Form from '../form/Form.vue';
|
||||
import FormAlert from '../form/FormAlert.vue';
|
||||
import FormField from '../form/FormField.vue';
|
||||
import FormSelectField from '../form/fields/FormSelectField.vue';
|
||||
import { SelectOption } from '../form/base/Select.vue';
|
||||
import { IsRequired, MinLength } from '../form/validators';
|
||||
import Modal from '../Modal.vue';
|
||||
import FormColorField from '../form/fields/FormColorField.vue';
|
||||
|
||||
const { authHeader } = useAccessToken();
|
||||
const defaults = {
|
||||
@ -52,6 +57,16 @@ const building = ref<BuildingListItem>();
|
||||
const set = ref<StorageSetListItem | boolean>();
|
||||
const data = ref({ ...defaults });
|
||||
const error = ref('');
|
||||
const selectOptions = computed(() => {
|
||||
const source = set.value === true ? StorageSetTypeName : StorageTypeName;
|
||||
return Object.keys(source).reduce<SelectOption[]>(
|
||||
(list, key) => [
|
||||
...list,
|
||||
{ value: key.toString(), name: source[key as keyof typeof source] },
|
||||
],
|
||||
[]
|
||||
);
|
||||
});
|
||||
|
||||
const validators = ref([
|
||||
{
|
||||
|
@ -12,3 +12,34 @@ export enum ItemType {
|
||||
SERVICE = 'SERVICE',
|
||||
OTHER = 'OTHER',
|
||||
}
|
||||
|
||||
export const ItemTypeName: Record<ItemType, string> = {
|
||||
[ItemType.ITEM]: 'Generic item',
|
||||
[ItemType.OBJECT]: 'Generic object',
|
||||
[ItemType.TECHNOLOGY]: 'Technology',
|
||||
[ItemType.TOOL]: 'Tool',
|
||||
[ItemType.FOOD]: 'Food',
|
||||
[ItemType.MEDICINE]: 'Medicine',
|
||||
[ItemType.ART]: 'Arts',
|
||||
[ItemType.CRAFT]: 'Crafts',
|
||||
[ItemType.COMPOSITION]: 'Composition/Creation',
|
||||
[ItemType.CLOTHING]: 'Clothing',
|
||||
[ItemType.SERVICE]: '(Virtual) Service',
|
||||
[ItemType.OTHER]: 'Other',
|
||||
};
|
||||
|
||||
export const ItemTypeDescription: Record<ItemType, string> = {
|
||||
[ItemType.ITEM]: 'An item, the classification is up to you',
|
||||
[ItemType.OBJECT]: 'An object, the classification is up to you',
|
||||
[ItemType.TECHNOLOGY]:
|
||||
'Such as computer hardware, electronic components, etc',
|
||||
[ItemType.TOOL]: 'Tools of any kind',
|
||||
[ItemType.FOOD]: 'Edible, drinkable or otherwise consumable',
|
||||
[ItemType.MEDICINE]: 'Medicine',
|
||||
[ItemType.ART]: 'Artworks, prints',
|
||||
[ItemType.CRAFT]: 'Crafted items',
|
||||
[ItemType.COMPOSITION]: 'A combination of items that created a new item',
|
||||
[ItemType.CLOTHING]: 'Clothing, wearables',
|
||||
[ItemType.SERVICE]: 'Online service, computer software, etc',
|
||||
[ItemType.OTHER]: 'Other',
|
||||
};
|
||||
|
@ -9,3 +9,15 @@ export enum StorageSetType {
|
||||
BOXES = 'BOXES',
|
||||
OTHER = 'OTHER',
|
||||
}
|
||||
|
||||
export const StorageSetTypeName: Record<StorageSetType, string> = {
|
||||
[StorageSetType.DRAWERS]: 'Set of drawers',
|
||||
[StorageSetType.SHELVES]: 'Shelves',
|
||||
[StorageSetType.CUPBOARD]: 'Cupboard',
|
||||
[StorageSetType.CLOSET]: 'Closet',
|
||||
[StorageSetType.FRIDGE]: 'Fridge',
|
||||
[StorageSetType.FREEZER]: 'Freezer',
|
||||
[StorageSetType.BOX]: 'Box',
|
||||
[StorageSetType.BOXES]: 'Boxes',
|
||||
[StorageSetType.OTHER]: 'Other',
|
||||
};
|
||||
|
@ -12,3 +12,18 @@ export enum StorageType {
|
||||
PERSON = 'PERSON',
|
||||
OTHER = 'OTHER',
|
||||
}
|
||||
|
||||
export const StorageTypeName: Record<StorageType, string> = {
|
||||
[StorageType.DRAWER]: 'Drawer',
|
||||
[StorageType.SHELF]: 'Shelf',
|
||||
[StorageType.ASSEMBLY]: 'Assembly',
|
||||
[StorageType.CUPBOARD]: 'Cupboard',
|
||||
[StorageType.CLOSET]: 'Closet',
|
||||
[StorageType.FRIDGE]: 'Fridge',
|
||||
[StorageType.FREEZER]: 'Freezer',
|
||||
[StorageType.TABLE]: 'Table',
|
||||
[StorageType.BOX]: 'Box',
|
||||
[StorageType.VIRTUAL]: 'Virtual',
|
||||
[StorageType.PERSON]: 'Person',
|
||||
[StorageType.OTHER]: 'Other',
|
||||
};
|
||||
|
@ -6,3 +6,24 @@ export enum TransactionType {
|
||||
MOVED = 'MOVED',
|
||||
TRANSFERRED = 'TRANSFERRED',
|
||||
}
|
||||
|
||||
export const TransactionTypeName: Record<TransactionType, string> = {
|
||||
[TransactionType.ACQUIRED]: 'Acquired / Purchased',
|
||||
[TransactionType.SOLD]: 'Sold',
|
||||
[TransactionType.DESTROYED]: 'Destroyed (got rid of)',
|
||||
[TransactionType.BINNED]: 'Binned (threw away)',
|
||||
[TransactionType.MOVED]: 'Moved',
|
||||
[TransactionType.TRANSFERRED]: 'Transferred',
|
||||
};
|
||||
|
||||
export const TransactionTypeDescription: Record<TransactionType, string> = {
|
||||
[TransactionType.ACQUIRED]:
|
||||
'Items were received into your possession by any means',
|
||||
[TransactionType.SOLD]:
|
||||
'Item was sold, optionally mark proceeds with negative price',
|
||||
[TransactionType.DESTROYED]:
|
||||
'Burned, broken beyond recognition, unusable etc',
|
||||
[TransactionType.BINNED]: 'Thrown away for recycle or otherwise',
|
||||
[TransactionType.MOVED]: 'Moved to another place / storage',
|
||||
[TransactionType.TRANSFERRED]: 'Transferred to another person / location',
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user