feat: use script setup for team views WIP
continuous-integration/drone/pr Build is failing
Details
continuous-integration/drone/pr Build is failing
Details
This commit is contained in:
parent
f61d5bac46
commit
04aa74a01f
|
@ -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",
|
||||
|
|
|
@ -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')
|
|
@ -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',
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
13
yarn.lock
13
yarn.lock
|
@ -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"
|
||||
|
|
Reference in New Issue