Skip to content

Commit

Permalink
Merge pull request #58 from traPtitech/feat/projects/project_id/member
Browse files Browse the repository at this point in the history
/projects/:projectId/members
  • Loading branch information
mehm8128 authored Jan 4, 2024
2 parents 517670b + 5f2bc87 commit 1134b37
Show file tree
Hide file tree
Showing 8 changed files with 294 additions and 81 deletions.
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;
}
.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;
flex-wrap: wrap;
gap: 0.5rem;
Expand Down Expand Up @@ -128,13 +143,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

0 comments on commit 1134b37

Please sign in to comment.