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

/projects/:projectId/members #58

Merged
merged 14 commits into from
Jan 4, 2024
71 changes: 71 additions & 0 deletions src/components/Projects/ProjectMember.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<script lang="ts" setup>
import { User, YearWithSemesterDuration } from '/@/lib/apis'
import UserIcon from '/@/components/UI/UserIcon.vue'
import FormProjectDuration from '/@/components/UI/FormProjectDuration.vue'
import Icon from '/@/components/UI/Icon.vue'
import { computed } from 'vue'
interface Props {
user: User
modelValue: YearWithSemesterDuration
}

const props = defineProps<Props>()

const value = computed({
get: () => props.modelValue,
set: v => emit('update:modelValue', v)
})

const emit = defineEmits<{
(e: 'delete', value: string): void
(e: 'update:modelValue', value: YearWithSemesterDuration): void
}>()
</script>

<template>
<div :class="$style.container">
<div :class="$style.user">
<user-icon :user-id="user.id" :size="48" />
<p :class="$style.name">{{ user.name }}</p>
</div>

<form-project-duration
v-model="value"
since-required
:class="$style.projectDuration"
/>
<button :class="$style.icon" @click="emit('delete', user.id)">
<icon :size="32" name="mdi:delete" />
</button>
</div>
</template>

<style lang="scss" module>
.container {
display: flex;
gap: 1rem;

align-items: center;
padding: 0.5rem;

border: 1px solid $color-primary-text;
border-radius: 8px;
}
.name {
word-break: break-all;
}
.user {
width: 30%;
display: flex;
align-items: center;
gap: 0.5rem;
mehm8128 marked this conversation as resolved.
Show resolved Hide resolved
}

.icon {
color: $color-secondary;
&:hover {
opacity: 0.8;
}
margin-left: auto;
}
</style>
41 changes: 33 additions & 8 deletions src/components/UI/BaseSelect.vue
Original file line number Diff line number Diff line change
@@ -1,30 +1,54 @@
<script lang="ts" setup>
<script lang="ts" setup generic="T">
import vSelect from 'vue-select'
import 'vue-select/dist/vue-select.css'
import { computed } from 'vue'
import Icon from '/@/components/UI/Icon.vue'

export interface Option {
export interface Option<T> {
label: string
value: string
value: T
}

interface Props {
modelValue: string
options: Option[]
modelValue: T
options: Option<T>[]
by?: keyof T | ((a: T, b: T) => boolean)
searchable?: boolean
}

const props = defineProps<Props>()

const emit = defineEmits<{
(e: 'update:modelValue', value: string): void
(e: 'update:modelValue', value: T): void
}>()

const value = computed({
get: () => props.modelValue,
set: v => emit('update:modelValue', v)
})

const compare = (a: T, b: T) => {
if (typeof props.by === 'function') {
return props.by(a, b)
}

if (typeof props.by === 'string') {
return a[props.by] === b[props.by]
}

if (
a !== null &&
b !== null &&
typeof a === 'object' &&
typeof b === 'object' &&
'id' in a &&
'id' in b
) {
return a.id === b.id
}

return a === b
}
</script>

<template>
Expand All @@ -33,14 +57,14 @@ const value = computed({
:options="options"
:clearable="false"
label="label"
:reduce="(option:Option) => option.value"
:reduce="(option: Option<T>) => option.value"
:class="$style.select"
:searchable="searchable"
>
<template #option="{ label }">
<div :class="$style.item">
<icon
v-if="label === options.find(o => o.value === value)?.label"
v-if="label === options.find(o => compare(o.value, value))?.label"
name="mdi:tick-circle-outline"
:class="$style.icon"
/>
Expand All @@ -60,6 +84,7 @@ const value = computed({
color: $color-primary;
}
.label {
display: grid;
grid-column: 2;
}
}
Expand Down
132 changes: 74 additions & 58 deletions src/components/UI/FormProjectDuration.vue
Original file line number Diff line number Diff line change
@@ -1,51 +1,80 @@
<script lang="ts" setup>
import { Semester, YearWithSemesterDuration } from '/@/lib/apis'
import {
Semester,
YearWithSemester,
YearWithSemesterDuration
} from '/@/lib/apis'
import RequiredChip from '/@/components/UI/RequiredChip.vue'
import { Option } from '/@/components/UI/BaseSelect.vue'
import BaseSelect from '/@/components/UI/BaseSelect.vue'

type DateType = 'sinceYear' | 'sinceSemester' | 'untilYear' | 'untilSemester'
type DateType = 'since' | 'until'

interface Props {
modelValue: YearWithSemesterDuration
yearsAgo?: number
sinceRequired?: boolean
}

const props = defineProps<Props>()
const props = withDefaults(defineProps<Props>(), {
yearsAgo: 20
})
const emit = defineEmits<{
(e: 'update:modelValue', modelValue: YearWithSemesterDuration): void
(
e: 'update:modelValue',
modelValue: {
since: YearWithSemester | undefined
until: YearWithSemester | undefined
}
): void
}>()

const yearOptions = Array(20)
const options: Option<YearWithSemester | undefined>[] = Array(props.yearsAgo)
.fill(null)
.map((_, i) => ({
label: (new Date().getFullYear() - i).toString(),
value: (new Date().getFullYear() - i).toString()
}))
const semesterOptions = [
{ label: '前期', value: Semester.first.toString() },
{ label: '後期', value: Semester.second.toString() }
]

const handleInput = (value: string, dateType: DateType) => {
const numValue = parseInt(value)
const duration: YearWithSemesterDuration = {
since: {
year: dateType === 'sinceYear' ? numValue : props.modelValue.since.year,
semester:
dateType === 'sinceSemester'
? numValue
: props.modelValue.since.semester
.flatMap((_, i) => [
{
label: `${(new Date().getFullYear() - i).toString()} 後期`,
value: {
year: new Date().getFullYear() - i,
semester: Semester.second
}
},
until: props.modelValue.until && {
year: dateType === 'untilYear' ? numValue : props.modelValue.until.year,
semester:
dateType === 'untilSemester'
? numValue
: props.modelValue.until.semester
{
label: `${(new Date().getFullYear() - i).toString()} 前期`,
value: {
year: new Date().getFullYear() - i,
semester: Semester.first
}
}
])

const untilOptions = [
{
label: '未定',
value: undefined
},
...options
]

const sinceOptions = props.sinceRequired ? options : untilOptions

const handleInput = (
value: YearWithSemester | undefined,
dateType: DateType
) => {
const duration = {
since: dateType === 'since' ? value : props.modelValue.since,
until: dateType === 'until' ? value : props.modelValue.until
}
emit('update:modelValue', duration)
}

const compare = (
a: YearWithSemester | undefined,
b: YearWithSemester | undefined
) => {
if (a === undefined && b === undefined) return true
if (a === undefined || b === undefined) return false
return a.year === b.year && a.semester === b.semester
}
</script>

<template>
Expand All @@ -56,18 +85,12 @@ const handleInput = (value: string, dateType: DateType) => {
<required-chip v-if="sinceRequired" />
</div>
<div :class="$style.form">
<div :class="$style.yearForm">
<base-select
:options="yearOptions"
:class="$style.yearInput"
:model-value="modelValue.since.year.toString()"
@update:model-value="handleInput($event, 'sinceYear')"
/>年
</div>
<base-select
:options="semesterOptions"
:model-value="modelValue.since.semester.toString()"
@update:model-value="handleInput($event, 'sinceSemester')"
:options="sinceOptions"
:class="$style.input"
:model-value="modelValue.since"
:by="compare"
@update:model-value="handleInput($event, 'since')"
/>
</div>
</div>
Expand All @@ -77,21 +100,12 @@ const handleInput = (value: string, dateType: DateType) => {
<p :class="$style.head">~まで</p>
</div>
<div :class="$style.form">
<div :class="$style.yearForm">
<base-select
:options="yearOptions"
:class="$style.yearInput"
:model-value="
modelValue.until?.year.toString() ??
new Date().getFullYear().toString()
"
@update:model-value="handleInput($event, 'untilYear')"
/>年
</div>
<base-select
:options="semesterOptions"
:model-value="modelValue.until?.semester.toString() ?? '前期'"
@update:model-value="handleInput($event, 'untilSemester')"
:options="untilOptions"
:class="$style.input"
:model-value="modelValue.until"
:by="compare"
@update:model-value="handleInput($event, 'until')"
/>
</div>
</div>
Expand All @@ -101,6 +115,7 @@ const handleInput = (value: string, dateType: DateType) => {
<style module lang="scss">
.container {
display: flex;
flex-wrap: wrap;
align-items: flex-end;
gap: 0.5rem;
}
Expand All @@ -127,13 +142,14 @@ const handleInput = (value: string, dateType: DateType) => {
display: flex;
flex-direction: column;
gap: 0.5rem;
flex-wrap: nowrap;
}
.yearForm {
display: flex;
align-items: center;
gap: 0.5rem;
}
.yearInput {
width: 8.75rem;
.input {
width: 10rem;
}
</style>
17 changes: 9 additions & 8 deletions src/components/UI/MemberInput.vue
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
<script lang="ts" setup>
<script lang="ts" setup generic="U extends User">
import vSelect from 'vue-select'
import 'vue-select/dist/vue-select.css'

import { computed, nextTick, onUnmounted, ref } from 'vue'
import { User } from '/@/lib/apis'
import { useUserStore } from '/@/store/user'
import UserIcon from '/@/components/UI/UserIcon.vue'

const userStore = useUserStore()
const users = await userStore.fetchUsers()

interface Props {
modelValue: User[]
modelValue: U[]
isDisabled: boolean
users: U[]
}

const props = defineProps<Props>()
const props = withDefaults(defineProps<Props>(), {
isDisabled: false
})

const emit = defineEmits<{
(e: 'update:modelValue', value: User[]): void
Expand All @@ -29,7 +29,7 @@ const limit = ref(10)
const search = ref('')

const filtered = computed(
() => users.filter(user => user.name.includes(search.value)) ?? []
() => props.users.filter(user => user.name.includes(search.value)) ?? []
)
const options = computed(() => filtered.value.slice(0, limit.value))
const hasNextPage = computed(() => filtered.value.length > options.value.length)
Expand Down Expand Up @@ -82,6 +82,7 @@ const onClose = () => {
multiple
:close-on-select="false"
deselect-from-dropdown
:disabled="isDisabled"
@open="onOpen"
@close="onClose"
@search="onSearch"
Expand Down
Loading