feat: use script setup for team views WIP
continuous-integration/drone/pr Build is failing Details

This commit is contained in:
Dominik Pschenitschni 2021-11-27 14:28:17 +01:00
parent f61d5bac46
commit 04aa74a01f
Signed by: dpschen
GPG Key ID: B257AC0149F43A77
12 changed files with 654 additions and 469 deletions

View File

@ -37,6 +37,7 @@
"lodash.clonedeep": "4.5.0",
"lodash.debounce": "4.0.8",
"marked": "4.0.5",
"pinia": "^2.0.4",
"register-service-worker": "1.7.2",
"snake-case": "3.0.4",
"ufo": "0.7.9",

View File

@ -33,6 +33,8 @@ import './registerServiceWorker'
// Vuex
import {store} from './store'
// Pinia
import { createPinia } from 'pinia'
// i18n
import {i18n} from './i18n'
@ -133,8 +135,9 @@ if (window.SENTRY_ENABLED) {
import('./sentry').then(sentry => sentry.default(app, router))
}
app.use(router)
app.use(store)
app.use(i18n)
app.use(store)
app.use(createPinia())
app.use(router)
app.mount('#app')

View File

@ -14,8 +14,7 @@ import LinkShareAuthComponent from '../views/sharing/LinkSharingAuth'
import TaskDetailViewModal from '../views/tasks/TaskDetailViewModal'
import TaskDetailView from '../views/tasks/TaskDetailView'
import ListNamespaces from '../views/namespaces/ListNamespaces'
// Team Handling
import ListTeamsComponent from '../views/teams/ListTeams'
// Label Handling
import ListLabelsComponent from '../views/labels/ListLabels'
import NewLabelComponent from '../views/labels/NewLabel'
@ -65,8 +64,10 @@ const NewListComponent = () => import('../views/list/NewList')
// Namespace Handling
const NewNamespaceComponent = () => import('../views/namespaces/NewNamespace')
const EditTeamComponent = () => import('../views/teams/EditTeam')
const NewTeamComponent = () => import('../views/teams/NewTeam')
// Team Handling
const Teams = () => import('@/views/teams/Teams')
const TeamsEdit = () => import('@/views/teams/TeamsEdit')
const TeamsNew = () => import('@/views/teams/TeamsNew')
const router = createRouter({
history: createWebHistory(),
@ -505,19 +506,19 @@ const router = createRouter({
{
path: '/teams',
name: 'teams.index',
component: ListTeamsComponent,
component: Teams,
},
{
path: '/teams/new',
name: 'teams.create',
components: {
popup: NewTeamComponent,
popup: TeamsNew,
},
},
{
path: '/teams/:id/edit',
name: 'teams.edit',
component: EditTeamComponent,
component: TeamsEdit,
},
{
path: '/labels',

View File

@ -1,5 +1,7 @@
import AbstractService from './abstractService'
import TeamModel from '../models/team'
import TeamModel from '@/models/team'
import {formatISO} from 'date-fns'
export default class TeamService extends AbstractService {

225
src/stores/teams.js Normal file
View File

@ -0,0 +1,225 @@
import { reactive, unref, watch, computed, watchEffect, shallowReactive } from 'vue'
import router from '@/router'
import { useI18n } from 'vue-i18n'
import TeamService from '@/services/team'
import TeamModel from '@/models/team'
import TeamMemberService from '@/services/teamMember'
import TeamMemberModel from '@/models/teamMember'
import Rights from '@/models/constants/rights.json'
import { success } from '@/message'
import { defineStore, storeToRefs, acceptHMRUpdate } from 'pinia'
// the first argument is a unique id of the store across your application
export const useTeamStore = defineStore('team', () => {
const { t } = useI18n()
const teamService = shallowReactive(new TeamService())
const teamServiceLoading = computed(() => teamService.loading)
const teamMemberService = shallowReactive(new TeamMemberService())
const teamMemberServiceLoading = computed(() => teamMemberService.loading)
const teams = reactive({})
const members = reactive({})
// create getters
function getTeamMembersByTeamId(teamId) {
return teams?.[teamId].memberIds.map((memberId) => members[memberId])
}
function setTeam(unformattedTeam) {
const { members, ...team } = unformattedTeam
setMembers(members)
team.memberIds = members.map(({ id }) => id)
teams[team.id] = team
return team
}
function setMembers(members) {
members.forEach((member) => {
members[member.id] = member
})
}
async function loadAllTeams() {
console.log('loadAllTeams')
const newTeams = await teamService.getAll()
newTeams.forEach((team) => setTeam(team))
console.log(newTeams)
console.log(teams)
}
async function loadTeam(teamId) {
setTeam(new TeamModel({ id: teamId }))
const unformattedTeam = await teamService.get(teams[teamId])
return setTeam(unformattedTeam)
}
async function newTeam(team) {
if (team.name === '') {
throw new Error(t('team.attributes.nameRequired'))
}
const newTeam = await teamService.create(team)
setTeam(newTeam)
router.push({
name: 'teams.edit',
params: { id: newTeam.id },
})
success({ message: t('team.create.success') })
}
async function updateTeam(team) {
if (team.name === '') {
throw new Error(t('team.attributes.nameRequired'))
}
const newTeam = new TeamMemberModel({
...team,
members: getTeamMembersByTeamId(team.id),
})
const unformattedTeam = await teamService.update(newTeam)
setTeam(unformattedTeam)
success({ message: t('team.edit.success') })
}
async function deleteTeam(teamId) {
await teamService.delete(teams[teamId])
delete teams[teamId]
success({ message: t('team.edit.delete.success') })
router.push({ name: 'teams.index' })
}
async function deleteTeamMember(teamMemberId) {
const teamId = members[teamMemberId].teamId
await teamMemberService.delete(members[teamMemberId])
teams[teamId].members = teams[teamId].members.filter(
(id) => id !== teamMemberId,
)
delete members[teamMemberId]
success({ message: t('team.edit.deleteUser.success') })
}
async function addTeamMember(user, teamId) {
const newMember = new TeamMemberModel({
teamId,
username: user.username,
})
const member = teamMemberService.create(newMember)
setMembers([member])
teams[teamId].members.push(member.id)
success({ message: t('team.edit.userAddedSuccess') })
}
async function toggleMemberType(memberId) {
const member = members[memberId]
const newMember = {
admin: !member.admin,
teamId: member.teamId,
}
const updatedMember = await teamMemberService.update(newMember)
setMembers([updatedMember])
// FIXME: update userservice ?
success({
message: member.admin
? t('team.edit.madeAdmin')
: t('team.edit.madeMember'),
})
}
watchEffect(() => loadAllTeams())
return {
// state
// TODO: add readonly()
teams,
// teams: readonly(teams),
members,
// members: readonly(members),
// getters
teamServiceLoading,
teamMemberServiceLoading,
// ACTIONS
// team
setMembers,
loadAllTeams,
loadTeam,
newTeam,
updateTeam,
deleteTeam,
// members
deleteTeamMember,
addTeamMember,
toggleMemberType,
}
})
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useTeamStore, import.meta.hot))
}
export function useTeam(teamId) {
const teamStore = useTeamStore()
const {
members,
addTeamMember,
deleteTeamMember,
newTeam,
updateTeam,
deleteTeam,
} = teamStore
const team = reactive(new TeamModel())
const isNewTeam = computed(() => Boolean(unref(teamId)))
watch(() => unref(teamId), () => {
if (isNewTeam.value) {
return
}
teamStore.loadTeam(unref(teamId)).then((loadedTeam) => {
Object.assign(team, loadedTeam)
})
})
const userIsAdmin = computed(() => team?.maxRight > Rights.READ)
const {teamServiceLoading, teamMemberServiceLoading} = storeToRefs(teamStore)
return {
teamServiceLoading,
teamMemberServiceLoading,
team,
members,
addTeamMember: (user) => addTeamMember(user, teamId),
deleteTeamMember,
newTeam: () => newTeam(team),
updateTeam: () => updateTeam(team),
deleteTeam: () => deleteTeam(teamId),
userIsAdmin,
}
}

View File

@ -1,311 +0,0 @@
<template>
<div
class="loader-container is-max-width-desktop"
:class="{ 'is-loading': teamService.loading }"
>
<card class="is-fullwidth" v-if="userIsAdmin" :title="title">
<form @submit.prevent="save()">
<div class="field">
<label class="label" for="teamtext">{{ $t('team.attributes.name') }}</label>
<div class="control">
<input
:class="{ disabled: teamMemberService.loading }"
:disabled="teamMemberService.loading || null"
class="input"
id="teamtext"
:placeholder="$t('team.attributes.namePlaceholder')"
type="text"
v-focus
v-model="team.name"
/>
</div>
</div>
<p
class="help is-danger"
v-if="showError && team.name === ''"
>
{{ $t('team.attributes.nameRequired') }}
</p>
<div class="field">
<label class="label" for="teamdescription">{{ $t('team.attributes.description') }}</label>
<div class="control">
<editor
:class="{ disabled: teamService.loading }"
:disabled="teamService.loading"
:preview-is-default="false"
id="teamdescription"
:placeholder="$t('team.attributes.descriptionPlaceholder')"
v-model="team.description"
/>
</div>
</div>
</form>
<div class="field has-addons mt-4">
<div class="control is-fullwidth">
<x-button
@click="save()"
:loading="teamService.loading"
class="is-fullwidth"
>
{{ $t('misc.save') }}
</x-button>
</div>
<div class="control">
<x-button
@click="showDeleteModal = true"
:loading="teamService.loading"
class="is-danger"
icon="trash-alt"
/>
</div>
</div>
</card>
<card class="is-fullwidth has-overflow" :title="$t('team.edit.members')" :padding="false">
<div class="p-4" v-if="userIsAdmin">
<div class="field has-addons">
<div class="control is-expanded">
<multiselect
:loading="userService.loading"
:placeholder="$t('team.edit.search')"
@search="findUser"
:search-results="foundUsers"
label="username"
v-model="newMember"
/>
</div>
<div class="control">
<x-button @click="addUser" icon="plus">
{{ $t('team.edit.addUser') }}
</x-button>
</div>
</div>
</div>
<table class="table has-actions is-striped is-hoverable is-fullwidth">
<tbody>
<tr :key="m.id" v-for="m in team.members">
<td>{{ m.getDisplayName() }}</td>
<td>
<template v-if="m.id === userInfo.id">
<b class="is-success">You</b>
</template>
</td>
<td class="type">
<template v-if="m.admin">
<span class="icon is-small">
<icon icon="lock"/>
</span>
{{ $t('team.attributes.admin') }}
</template>
<template v-else>
<span class="icon is-small">
<icon icon="user"/>
</span>
{{ $t('team.attributes.member') }}
</template>
</td>
<td class="actions" v-if="userIsAdmin">
<x-button
:loading="teamMemberService.loading"
@click="() => toggleUserType(m)"
class="mr-2"
v-if="m.id !== userInfo.id"
>
{{ m.admin ? $t('team.edit.makeMember') : $t('team.edit.makeAdmin') }}
</x-button>
<x-button
:loading="teamMemberService.loading"
@click="() => {member = m; showUserDeleteModal = true}"
class="is-danger"
v-if="m.id !== userInfo.id"
icon="trash-alt"
/>
</td>
</tr>
</tbody>
</table>
</card>
<!-- Team delete modal -->
<transition name="modal">
<modal
@close="showDeleteModal = false"
@submit="deleteTeam()"
v-if="showDeleteModal"
>
<template #header><span>{{ $t('team.edit.delete.header') }}</span></template>
<template #text>
<p>{{ $t('team.edit.delete.text1') }}<br/>
{{ $t('team.edit.delete.text2') }}</p>
</template>
</modal>
</transition>
<!-- User delete modal -->
<transition name="modal">
<modal
@close="showUserDeleteModal = false"
@submit="deleteUser()"
v-if="showUserDeleteModal"
>
<template #header><span>{{ $t('team.edit.deleteUser.header') }}</span></template>
<template #text>
<p>{{ $t('team.edit.deleteUser.text1') }}<br/>
{{ $t('team.edit.deleteUser.text2') }}</p>
</template>
</modal>
</transition>
</div>
</template>
<script>
import AsyncEditor from '@/components/input/AsyncEditor'
import {mapState} from 'vuex'
import { i18n } from '@/i18n'
import TeamService from '../../services/team'
import TeamModel from '../../models/team'
import TeamMemberService from '../../services/teamMember'
import TeamMemberModel from '../../models/teamMember'
import UserModel from '../../models/user'
import UserService from '../../services/user'
import Rights from '../../models/constants/rights.json'
import Multiselect from '@/components/input/multiselect.vue'
export default {
name: 'EditTeam',
data() {
return {
teamService: new TeamService(),
teamMemberService: new TeamMemberService(),
team: TeamModel,
teamId: this.$route.params.id,
member: TeamMemberModel,
showDeleteModal: false,
showUserDeleteModal: false,
newMember: UserModel,
foundUsers: [],
userService: new UserService(),
showError: false,
title: '',
}
},
components: {
Multiselect,
editor: AsyncEditor,
},
watch: {
// call again the method if the route changes
'$route': {
handler: 'loadTeam',
deep: true,
immediate: true,
},
},
computed: {
userIsAdmin() {
return (
this.team &&
this.team.maxRight &&
this.team.maxRight > Rights.READ
)
},
...mapState({
userInfo: (state) => state.auth.info,
}),
},
methods: {
async loadTeam() {
this.team = new TeamModel({id: this.teamId})
this.team = await this.teamService.get(this.team)
this.title = i18n.global.t('team.edit.title', {team: this.team.name})
this.setTitle(this.title)
},
async save() {
if (this.team.name === '') {
this.showError = true
return
}
this.showError = false
this.team = await this.teamService.update(this.team)
this.$message.success({message: this.$t('team.edit.success')})
},
async deleteTeam() {
await this.teamService.delete(this.team)
this.$message.success({message: this.$t('team.edit.delete.success')})
this.$router.push({name: 'teams.index'})
},
async deleteUser() {
try {
await this.teamMemberService.delete(this.member)
this.$message.success({message: this.$t('team.edit.deleteUser.success')})
this.loadTeam()
} finally {
this.showUserDeleteModal = false
}
},
async addUser() {
const newMember = new TeamMemberModel({
teamId: this.teamId,
username: this.newMember.username,
})
await this.teamMemberService.create(newMember)
this.loadTeam()
this.$message.success({message: this.$t('team.edit.userAddedSuccess')})
},
async toggleUserType(member) {
// FIXME: direct manipulation
member.admin = !member.admin
member.teamId = this.teamId
const r = await this.teamMemberService.update(member)
for (const tm in this.team.members) {
if (this.team.members[tm].id === member.id) {
this.team.members[tm].admin = r.admin
break
}
}
this.$message.success({
message: member.admin ?
this.$t('team.edit.madeAdmin') :
this.$t('team.edit.madeMember'),
})
},
async findUser(query) {
if (query === '') {
this.clearAll()
return
}
this.foundUsers = await this.userService.getAll({}, {s: query})
},
clearAll() {
this.foundUsers = []
},
},
}
</script>
<style lang="scss" scoped>
.card.is-fullwidth {
margin-bottom: 1rem;
.content {
padding: 0;
}
}
</style>

View File

@ -1,80 +0,0 @@
<template>
<div class="content loader-container is-max-width-desktop" :class="{ 'is-loading': teamService.loading}">
<x-button
:to="{name:'teams.create'}"
class="is-pulled-right"
icon="plus"
>
{{ $t('team.create.title') }}
</x-button>
<h1>{{ $t('team.title') }}</h1>
<ul class="teams box" v-if="teams.length > 0">
<li :key="t.id" v-for="t in teams">
<router-link :to="{name: 'teams.edit', params: {id: t.id}}">
{{ t.name }}
</router-link>
</li>
</ul>
<p v-else-if="!teamService.loading" class="has-text-centered has-text-grey is-italic">
{{ $t('team.noTeams') }}
<router-link :to="{name: 'teams.create'}">
{{ $t('team.create.title') }}.
</router-link>
</p>
</div>
</template>
<script>
import TeamService from '../../services/team'
export default {
name: 'ListTeams',
data() {
return {
teamService: new TeamService(),
teams: [],
}
},
created() {
this.loadTeams()
},
mounted() {
this.setTitle(this.$t('team.title'))
},
methods: {
async loadTeams() {
this.teams = await this.teamService.getAll()
},
},
}
</script>
<style lang="scss" scoped>
ul.teams {
padding: 0;
margin-left: 0;
overflow: hidden;
li {
list-style: none;
margin: 0;
border-bottom: 1px solid $border;
a {
color: #363636;
display: block;
padding: 0.5rem 1rem;
transition: background-color $transition;
&:hover {
background: var(--grey-100);
}
}
}
li:last-child {
border-bottom: none;
}
}
</style>

View File

@ -1,68 +0,0 @@
<template>
<create-edit
:title="$t('team.create.title')"
@create="newTeam()"
:primary-disabled="team.name === ''"
>
<div class="field">
<label class="label" for="teamName">{{ $t('team.attributes.name') }}</label>
<div
class="control is-expanded"
:class="{ 'is-loading': teamService.loading }"
>
<input
:class="{ 'disabled': teamService.loading }"
class="input"
id="teamName"
:placeholder="$t('team.attributes.namePlaceholder')"
type="text"
v-focus
v-model="team.name"
@keyup.enter="newTeam"
/>
</div>
</div>
<p class="help is-danger" v-if="showError && team.name === ''">
{{ $t('team.attributes.nameRequired') }}
</p>
</create-edit>
</template>
<script>
import TeamModel from '../../models/team'
import TeamService from '../../services/team'
import CreateEdit from '@/components/misc/create-edit.vue'
export default {
name: 'NewTeam',
data() {
return {
teamService: new TeamService(),
team: new TeamModel(),
showError: false,
}
},
components: {
CreateEdit,
},
mounted() {
this.setTitle(this.$t('team.create.title'))
},
methods: {
async newTeam() {
if (this.team.name === '') {
this.showError = true
return
}
this.showError = false
const response = await this.teamService.create(this.team)
this.$router.push({
name: 'teams.edit',
params: { id: response.id },
})
this.$message.success({message: this.$t('team.create.success') })
},
},
}
</script>

82
src/views/teams/Teams.vue Normal file
View File

@ -0,0 +1,82 @@
<template>
<div class="content loader-container is-max-width-desktop" :class="{ 'is-loading': teamServiceLoading}">
<x-button
:to="{name:'teams.create'}"
class="is-pulled-right"
icon="plus"
>
{{ $t('team.create.title') }}
</x-button>
<h1>{{ $t('team.title') }}</h1>
<ul class="team-list box" v-if="Object.keys(teams).length > 0">
<li class="team-item" :key="t.id" v-for="t in teams">
<router-link class="team-link" :to="{name: 'teams.edit', params: {id: t.id}}">
{{ t.name }}
</router-link>
</li>
</ul>
<p v-else-if="!teamServiceLoading" class="has-text-centered has-text-grey is-italic">
{{ $t('team.noTeams') }}
<router-link :to="{name: 'teams.create'}">
{{ $t('team.create.title') }}.
</router-link>
</p>
</div>
</template>
<script setup>
import { watchEffect } from 'vue'
import { useI18n } from 'vue-i18n'
import { storeToRefs } from 'pinia'
import { useTitle } from '@/composables/useTitle'
import { useTeamStore } from '@/stores/teams'
const { t } = useI18n()
useTitle(() => t('team.title'))
function useTeams() {
const teamStore = useTeamStore()
watchEffect(() => teamStore.loadAllTeams())
const {teamServiceLoading} = storeToRefs(teamStore)
return {
teams: teamStore.teams,
teamServiceLoading,
}
}
const {teams, teamServiceLoading} = useTeams()
</script>
<style lang="scss" scoped>
.team-list {
padding: 0;
margin-left: 0;
overflow: hidden;
}
.team-item {
list-style: none;
margin: 0;
> & + & {
border-top: 1px solid $border;
}
}
.team-link {
color: #363636;
display: block;
padding: 0.5rem 1rem;
transition: background-color $transition;
&:hover {
background: var(--grey-100);
}
}
</style>

View File

@ -0,0 +1,259 @@
<template>
<div
class="loader-container is-max-width-desktop"
:class="{ 'is-loading': teamServiceLoading }"
>
<card class="is-fullwidth" v-if="userIsAdmin" :title="title">
<form @submit.prevent="saveTeam()">
<div class="field">
<label class="label" for="teamtext">{{ $t('team.attributes.name') }}</label>
<div class="control">
<input
:class="{ disabled: teamMemberServiceLoading }"
:disabled="teamMemberServiceLoading || null"
class="input"
id="teamtext"
:placeholder="$t('team.attributes.namePlaceholder')"
type="text"
v-focus
v-model="team.name"
/>
</div>
</div>
<p
class="help is-danger"
v-if="showError && team.name === ''"
>
{{ $t('team.attributes.nameRequired') }}
</p>
<div class="field">
<label class="label" for="teamdescription">{{ $t('team.attributes.description') }}</label>
<div class="control">
<editor
:class="{ disabled: teamServiceLoading }"
:disabled="teamServiceLoading"
:preview-is-default="false"
id="teamdescription"
:placeholder="$t('team.attributes.descriptionPlaceholder')"
v-model="team.description"
/>
</div>
</div>
</form>
<div class="field has-addons mt-4">
<div class="control is-fullwidth">
<x-button
@click="saveTeam()"
:loading="teamServiceLoading"
class="is-fullwidth"
>
{{ $t('misc.save') }}
</x-button>
</div>
<div class="control">
<x-button
@click="openTeamDeleteDialog()"
:loading="teamServiceLoading"
class="is-danger"
icon="trash-alt"
/>
</div>
</div>
</card>
<card class="is-fullwidth has-overflow" :title="$t('team.edit.members')" :padding="false">
<div class="p-4" v-if="userIsAdmin">
<div class="field has-addons">
<div class="control is-expanded">
<multiselect
:loading="userService.loading"
:placeholder="$t('team.edit.search')"
@search="findUser"
:search-results="foundUsers"
label="username"
v-model="newMember"
/>
</div>
<div class="control">
<x-button @click="addTeamMember()" icon="plus">
{{ $t('team.edit.addUser') }}
</x-button>
</div>
</div>
</div>
<table class="table has-actions is-striped is-hoverable is-fullwidth">
<tbody>
<tr :key="m.id" v-for="m in members">
<td>{{ m.getDisplayName() }}</td>
<td>
<template v-if="m.id === userInfo.id">
<b class="is-success">You</b>
</template>
</td>
<td class="type">
<template v-if="m.admin">
<span class="icon is-small">
<icon icon="lock"/>
</span>
{{ $t('team.attributes.admin') }}
</template>
<template v-else>
<span class="icon is-small">
<icon icon="user"/>
</span>
{{ $t('team.attributes.member') }}
</template>
</td>
<td class="actions" v-if="userIsAdmin && m.id !== userInfo.id">
<x-button
:loading="teamMemberServiceLoading"
@click="() => toggleMemberType(m)"
class="mr-2"
>
{{ m.admin ? $t('team.edit.makeMember') : $t('team.edit.makeAdmin') }}
</x-button>
<x-button
:loading="teamMemberServiceLoading"
@click="openTeamMemberDeleteDialog(m)"
class="is-danger"
icon="trash-alt"
/>
</td>
</tr>
</tbody>
</table>
</card>
<!-- Team delete modal -->
<transition name="modal">
<modal
@close="showTeamDeleteDialog = false"
@submit="deleteTeam()"
v-if="showTeamDeleteDialog"
>
<template #header><span>{{ $t('team.edit.delete.header') }}</span></template>
<template #text>
<p>{{ $t('team.edit.delete.text1') }}<br/>
{{ $t('team.edit.delete.text2') }}</p>
</template>
</modal>
</transition>
<!-- User delete modal -->
<transition name="modal">
<modal
v-if="showUserDeleteDialog"
@close="showUserDeleteDialog = false"
@submit="deleteTeamMember()"
>
<template #header><span>{{ $t('team.edit.deleteUser.header') }}</span></template>
<template #text>
<p>{{ $t('team.edit.deleteUser.text1') }}<br/>
{{ $t('team.edit.deleteUser.text2') }}</p>
</template>
</modal>
</transition>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute } from 'vue-router'
import { store } from '@/store'
import {default as Editor} from '@/components/input/AsyncEditor'
import Multiselect from '@/components/input/multiselect.vue'
import UserModel from '@/models/user'
import UserService from '@/services/user'
import { useTeam } from '@/stores/teams'
import { useTitle } from '@/composables/useTitle'
const route = useRoute()
const teamId = computed(() => route.params.id)
const {
teamServiceLoading,
teamMemberServiceLoading,
team,
members,
addTeamMember,
deleteTeamMember: deleteTeamMemberAction,
updateTeam,
deleteTeam,
userIsAdmin,
} = useTeam(teamId)
const { t } = useI18n()
const title = useTitle(() => t('team.edit.title', {team: team.name}))
const newMember = ref(new UserModel())
const showError = ref(false)
async function saveTeam() {
showError.value = false
try {
await updateTeam()
} catch (e) {
if (e.message === t('team.attributes.nameRequired')) {
showError.value = true
}
}
}
const userInfo = computed(() => store.state.auth.info)
const userService = new UserService()
const foundUsers = ref([])
async function findUser(query) {
if (query === '') {
foundUsers.value = []
}
foundUsers.value = await userService.getAll({}, {s: query})
}
const showTeamDeleteDialog = ref(false)
function openTeamDeleteDialog() {
// FIXME: the delete dialog should be opened by a method and not via state change
showTeamDeleteDialog.value = true
}
const memberIdToDelete = ref(null)
const showUserDeleteDialog = ref(false)
function openTeamMemberDeleteDialog(memberId) {
memberIdToDelete.value = memberId
// FIXME: the delete dialog should be opened by a method and not via state change
showUserDeleteDialog.value = true
}
async function deleteTeamMember() {
try {
await deleteTeamMemberAction(memberIdToDelete.value)
} finally {
showUserDeleteDialog.value = false
}
}
</script>
<style lang="scss" scoped>
.card.is-fullwidth {
margin-bottom: 1rem;
.content {
padding: 0;
}
}
</style>

View File

@ -0,0 +1,58 @@
<template>
<create-edit
:title="$t('team.create.title')"
@create="newTeam()"
:primary-disabled="team.name === ''"
>
<div class="field">
<label class="label" for="teamName">{{ $t('team.attributes.name') }}</label>
<div
class="control is-expanded"
:class="{ 'is-loading': teamServiceLoading }"
>
<input
:class="{ 'disabled': teamServiceLoading }"
class="input"
id="teamName"
:placeholder="$t('team.attributes.namePlaceholder')"
type="text"
v-focus
v-model="team.name"
@keyup.enter="newTeam"
/>
</div>
</div>
<p class="help is-danger" v-if="showError && team.name === ''">
{{ $t('team.attributes.nameRequired') }}
</p>
</create-edit>
</template>
<script setup>
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import CreateEdit from '@/components/misc/create-edit.vue'
import { useTeam } from '@/stores/teams'
import { useTitle } from '@/composables/useTitle'
const { t } = useI18n()
useTitle(() => t('team.create.title'))
const showError = ref(false)
const {teamServiceLoading, team, newTeam: newTeamAction} = useTeam()
async function newTeam() {
try {
await newTeamAction()
} catch(e) {
if (e.message === t('team.attributes.nameRequired')) {
showError.value = true
} else {
throw e
}
}
}
</script>

View File

@ -3682,6 +3682,11 @@
resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.0.0-beta.19.tgz#f8e88059daa424515992426a0c7ea5cde07e99bf"
integrity sha512-ObzQhgkoVeoyKv+e8+tB/jQBL2smtk/NmC9OmFK8UqdDpoOdv/Kf9pyDWL+IFyM7qLD2C75rszJujvGSPSpGlw==
"@vue/devtools-api@^6.0.0-beta.20.1":
version "6.0.0-beta.20.1"
resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.0.0-beta.20.1.tgz#5b499647e929c35baf2a66a399578f9aa4601142"
integrity sha512-R2rfiRY+kZugzWh9ZyITaovx+jpU4vgivAEAiz80kvh3yviiTU3CBuGuyWpSwGz9/C7TkSWVM/FtQRGlZ16n8Q==
"@vue/eslint-config-typescript@9.1.0":
version "9.1.0"
resolved "https://registry.yarnpkg.com/@vue/eslint-config-typescript/-/eslint-config-typescript-9.1.0.tgz#b98a64352b312085444a08b98728962e2a8425ab"
@ -11231,6 +11236,14 @@ pify@^4.0.1:
resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231"
integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==
pinia@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/pinia/-/pinia-2.0.4.tgz#06f6a03f6f19e6ec8b63cc06459011d96948e53d"
integrity sha512-nAc2f9HmOcBbWRlnGDuBGedM1G6uFAR10FnJWP1/dgm1I2tM5jbgKL/3IgynP4mBnPCy//ky7g0WpCZl5Mmxsg==
dependencies:
"@vue/devtools-api" "^6.0.0-beta.20.1"
vue-demi "*"
pinkie-promise@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa"