1
0

chore: move frontend files

This commit is contained in:
kolaente
2024-02-07 14:56:56 +01:00
parent 447641c222
commit fc4676315d
606 changed files with 0 additions and 0 deletions

View File

@ -0,0 +1,328 @@
<template>
<div>
<p class="has-text-weight-bold">
{{ $t('project.share.links.title') }}
<span
v-tooltip="$t('project.share.links.explanation')"
class="is-size-7 has-text-grey is-italic ml-3"
>
{{ $t('project.share.links.what') }}
</span>
</p>
<div class="sharables-project">
<x-button
v-if="!(linkShares.length === 0 || showNewForm)"
icon="plus"
class="mb-4"
@click="showNewForm = true"
>
{{ $t('project.share.links.create') }}
</x-button>
<div
v-if="linkShares.length === 0 || showNewForm"
class="p-4"
>
<div class="field">
<label
class="label"
for="linkShareRight"
>
{{ $t('project.share.right.title') }}
</label>
<div class="control">
<div class="select">
<select
id="linkShareRight"
v-model="selectedRight"
>
<option :value="RIGHTS.READ">
{{ $t('project.share.right.read') }}
</option>
<option :value="RIGHTS.READ_WRITE">
{{ $t('project.share.right.readWrite') }}
</option>
<option :value="RIGHTS.ADMIN">
{{ $t('project.share.right.admin') }}
</option>
</select>
</div>
</div>
</div>
<div class="field">
<label
class="label"
for="linkShareName"
>
{{ $t('project.share.links.name') }}
</label>
<div class="control">
<input
id="linkShareName"
v-model="name"
v-tooltip="$t('project.share.links.nameExplanation')"
class="input"
:placeholder="$t('project.share.links.namePlaceholder')"
>
</div>
</div>
<div class="field">
<label
class="label"
for="linkSharePassword"
>
{{ $t('project.share.links.password') }}
</label>
<div class="control">
<input
id="linkSharePassword"
v-model="password"
v-tooltip="$t('project.share.links.passwordExplanation')"
type="password"
class="input"
:placeholder="$t('user.auth.passwordPlaceholder')"
>
</div>
</div>
<x-button
icon="plus"
@click="add(projectId)"
>
{{ $t('project.share.share') }}
</x-button>
</div>
<table
v-if="linkShares.length > 0"
class="table has-actions is-striped is-hoverable is-fullwidth"
>
<thead>
<tr>
<th />
<th>{{ $t('project.share.links.view') }}</th>
<th>{{ $t('project.share.attributes.delete') }}</th>
</tr>
</thead>
<tbody>
<tr
v-for="s in linkShares"
:key="s.id"
>
<td>
<p
v-if="s.name !== ''"
class="mb-2 is-italic"
>
{{ s.name }}
</p>
<p class="mb-2">
<i18n-t
keypath="project.share.links.sharedBy"
scope="global"
>
<strong>{{ getDisplayName(s.sharedBy) }}</strong>
</i18n-t>
</p>
<p class="mb-2">
<template v-if="s.right === RIGHTS.ADMIN">
<span class="icon is-small">
<icon icon="lock" />
</span>&nbsp;
{{ $t('project.share.right.admin') }}
</template>
<template v-else-if="s.right === RIGHTS.READ_WRITE">
<span class="icon is-small">
<icon icon="pen" />
</span>&nbsp;
{{ $t('project.share.right.readWrite') }}
</template>
<template v-else>
<span class="icon is-small">
<icon icon="users" />
</span>&nbsp;
{{ $t('project.share.right.read') }}
</template>
</p>
<div class="field has-addons no-input-mobile">
<div class="control">
<input
:value="getShareLink(s.hash, selectedView[s.id])"
class="input"
readonly
type="text"
>
</div>
<div class="control">
<x-button
v-tooltip="$t('misc.copy')"
:shadow="false"
@click="copy(getShareLink(s.hash, selectedView[s.id]))"
>
<span class="icon">
<icon icon="paste" />
</span>
</x-button>
</div>
</div>
</td>
<td>
<div class="select">
<select v-model="selectedView[s.id]">
<option
v-for="(title, key) in availableViews"
:key="key"
:value="key"
>
{{ title }}
</option>
</select>
</div>
</td>
<td class="actions">
<x-button
class="is-danger"
icon="trash-alt"
@click="
() => {
linkIdToDelete = s.id
showDeleteModal = true
}
"
/>
</td>
</tr>
</tbody>
</table>
</div>
<modal
:enabled="showDeleteModal"
@close="showDeleteModal = false"
@submit="remove(projectId)"
>
<template #header>
<span>{{ $t('project.share.links.remove') }}</span>
</template>
<template #text>
<p>{{ $t('project.share.links.removeText') }}</p>
</template>
</modal>
</div>
</template>
<script setup lang="ts">
import {ref, watch, computed, shallowReactive} from 'vue'
import {useI18n} from 'vue-i18n'
import {RIGHTS} from '@/constants/rights'
import LinkShareModel from '@/models/linkShare'
import type {ILinkShare} from '@/modelTypes/ILinkShare'
import type {IProject} from '@/modelTypes/IProject'
import LinkShareService from '@/services/linkShare'
import {useCopyToClipboard} from '@/composables/useCopyToClipboard'
import {success} from '@/message'
import {getDisplayName} from '@/models/user'
import type {ProjectView} from '@/types/ProjectView'
import {PROJECT_VIEWS} from '@/types/ProjectView'
import {useConfigStore} from '@/stores/config'
const props = defineProps({
projectId: {
default: 0,
required: false,
},
})
const {t} = useI18n({useScope: 'global'})
const linkShares = ref<ILinkShare[]>([])
const linkShareService = shallowReactive(new LinkShareService())
const selectedRight = ref(RIGHTS.READ)
const name = ref('')
const password = ref('')
const showDeleteModal = ref(false)
const linkIdToDelete = ref(0)
const showNewForm = ref(false)
type SelectedViewMapper = Record<IProject['id'], ProjectView>
const selectedView = ref<SelectedViewMapper>({})
const availableViews = computed<Record<ProjectView, string>>(() => ({
list: t('project.list.title'),
gantt: t('project.gantt.title'),
table: t('project.table.title'),
kanban: t('project.kanban.title'),
}))
const copy = useCopyToClipboard()
watch(
() => props.projectId,
load,
{immediate: true},
)
const configStore = useConfigStore()
const frontendUrl = computed(() => configStore.frontendUrl)
async function load(projectId: IProject['id']) {
// If projectId == 0 the project on the calling component wasn't already loaded, so we just bail out here
if (projectId === 0) {
return
}
const links = await linkShareService.getAll({projectId})
links.forEach((l: ILinkShare) => {
selectedView.value[l.id] = 'list'
})
linkShares.value = links
}
async function add(projectId: IProject['id']) {
const newLinkShare = new LinkShareModel({
right: selectedRight.value,
projectId,
name: name.value,
password: password.value,
})
await linkShareService.create(newLinkShare)
selectedRight.value = RIGHTS.READ
name.value = ''
password.value = ''
showNewForm.value = false
success({message: t('project.share.links.createSuccess')})
await load(projectId)
}
async function remove(projectId: IProject['id']) {
try {
await linkShareService.delete(new LinkShareModel({
id: linkIdToDelete.value,
projectId,
}))
success({message: t('project.share.links.deleteSuccess')})
await load(projectId)
} finally {
showDeleteModal.value = false
}
}
function getShareLink(hash: string, view: ProjectView = PROJECT_VIEWS.LIST) {
return frontendUrl.value + 'share/' + hash + '/auth?view=' + view
}
</script>
<style lang="scss" scoped>
// FIXME: I think this is not needed
.sharables-project:not(.card-content) {
overflow-y: auto
}
</style>

View File

@ -0,0 +1,373 @@
<template>
<div>
<p class="has-text-weight-bold">
{{ $t('project.share.userTeam.shared', {type: shareTypeNames}) }}
</p>
<div v-if="userIsAdmin">
<div class="field has-addons">
<p
class="control is-expanded"
:class="{ 'is-loading': searchService.loading }"
>
<Multiselect
v-model="sharable"
:loading="searchService.loading"
:placeholder="$t('misc.searchPlaceholder')"
:search-results="found"
:label="searchLabel"
@search="find"
/>
</p>
<p class="control">
<x-button @click="add()">
{{ $t('project.share.share') }}
</x-button>
</p>
</div>
</div>
<table
v-if="sharables.length > 0"
class="table has-actions is-striped is-hoverable is-fullwidth mb-4"
>
<tbody>
<tr
v-for="s in sharables"
:key="s.id"
>
<template v-if="shareType === 'user'">
<td>{{ getDisplayName(s) }}</td>
<td>
<template v-if="s.id === userInfo.id">
<b class="is-success">{{ $t('project.share.userTeam.you') }}</b>
</template>
</td>
</template>
<template v-if="shareType === 'team'">
<td>
<router-link
:to="{
name: 'teams.edit',
params: { id: s.id },
}"
>
{{ s.name }}
</router-link>
</td>
</template>
<td class="type">
<template v-if="s.right === RIGHTS.ADMIN">
<span class="icon is-small">
<icon icon="lock" />
</span>
{{ $t('project.share.right.admin') }}
</template>
<template v-else-if="s.right === RIGHTS.READ_WRITE">
<span class="icon is-small">
<icon icon="pen" />
</span>
{{ $t('project.share.right.readWrite') }}
</template>
<template v-else>
<span class="icon is-small">
<icon icon="users" />
</span>
{{ $t('project.share.right.read') }}
</template>
</td>
<td
v-if="userIsAdmin"
class="actions"
>
<div class="select">
<select
v-model="selectedRight[s.id]"
class="mr-2"
@change="toggleType(s)"
>
<option
:selected="s.right === RIGHTS.READ"
:value="RIGHTS.READ"
>
{{ $t('project.share.right.read') }}
</option>
<option
:selected="s.right === RIGHTS.READ_WRITE"
:value="RIGHTS.READ_WRITE"
>
{{ $t('project.share.right.readWrite') }}
</option>
<option
:selected="s.right === RIGHTS.ADMIN"
:value="RIGHTS.ADMIN"
>
{{ $t('project.share.right.admin') }}
</option>
</select>
</div>
<x-button
class="is-danger"
icon="trash-alt"
@click="
() => {
sharable = s
showDeleteModal = true
}
"
/>
</td>
</tr>
</tbody>
</table>
<Nothing v-else>
{{ $t('project.share.userTeam.notShared', {type: shareTypeNames}) }}
</Nothing>
<modal
:enabled="showDeleteModal"
@close="showDeleteModal = false"
@submit="deleteSharable()"
>
<template #header>
<span>{{
$t('project.share.userTeam.removeHeader', {type: shareTypeName, sharable: sharableName})
}}</span>
</template>
<template #text>
<p>{{ $t('project.share.userTeam.removeText', {type: shareTypeName, sharable: sharableName}) }}</p>
</template>
</modal>
</div>
</template>
<script lang="ts">
export default {name: 'UserTeamShare'}
</script>
<script setup lang="ts">
import {ref, reactive, computed, shallowReactive, type Ref} from 'vue'
import type {PropType} from 'vue'
import {useI18n} from 'vue-i18n'
import UserProjectService from '@/services/userProject'
import UserProjectModel from '@/models/userProject'
import type {IUserProject} from '@/modelTypes/IUserProject'
import UserService from '@/services/user'
import UserModel, { getDisplayName } from '@/models/user'
import type {IUser} from '@/modelTypes/IUser'
import TeamProjectService from '@/services/teamProject'
import TeamProjectModel from '@/models/teamProject'
import type { ITeamProject } from '@/modelTypes/ITeamProject'
import TeamService from '@/services/team'
import TeamModel from '@/models/team'
import type {ITeam} from '@/modelTypes/ITeam'
import {RIGHTS} from '@/constants/rights'
import Multiselect from '@/components/input/multiselect.vue'
import Nothing from '@/components/misc/nothing.vue'
import {success} from '@/message'
import {useAuthStore} from '@/stores/auth'
// FIXME: I think this whole thing can now only manage user/team sharing for projects? Maybe remove a little generalization?
const props = defineProps({
type: {
type: String as PropType<'project'>,
default: '',
},
shareType: {
type: String as PropType<'user' | 'team'>,
default: '',
},
id: {
type: Number,
default: 0,
},
userIsAdmin: {
type: Boolean,
default: false,
},
})
const {t} = useI18n({useScope: 'global'})
// This user service is a userProjectService, depending on the type we are using
let stuffService: UserProjectService | TeamProjectService
let stuffModel: IUserProject | ITeamProject
let searchService: UserService | TeamService
let sharable: Ref<IUser | ITeam>
const searchLabel = ref('')
const selectedRight = ref({})
// This holds either teams or users who this namepace or project is shared with
const sharables = ref([])
const showDeleteModal = ref(false)
const authStore = useAuthStore()
const userInfo = computed(() => authStore.info)
function createShareTypeNameComputed(count: number) {
return computed(() => {
if (props.shareType === 'user') {
return t('project.share.userTeam.typeUser', count)
}
if (props.shareType === 'team') {
return t('project.share.userTeam.typeTeam', count)
}
return ''
})
}
const shareTypeNames = createShareTypeNameComputed(2)
const shareTypeName = createShareTypeNameComputed(1)
const sharableName = computed(() => {
if (props.type === 'project') {
return t('project.list.title')
}
return ''
})
if (props.shareType === 'user') {
searchService = shallowReactive(new UserService())
// eslint-disable-next-line vue/no-ref-as-operand
sharable = ref(new UserModel())
searchLabel.value = 'username'
if (props.type === 'project') {
stuffService = shallowReactive(new UserProjectService())
stuffModel = reactive(new UserProjectModel({projectId: props.id}))
} else {
throw new Error('Unknown type: ' + props.type)
}
} else if (props.shareType === 'team') {
searchService = new TeamService()
// eslint-disable-next-line vue/no-ref-as-operand
sharable = ref(new TeamModel())
searchLabel.value = 'name'
if (props.type === 'project') {
stuffService = shallowReactive(new TeamProjectService())
stuffModel = reactive(new TeamProjectModel({projectId: props.id}))
} else {
throw new Error('Unknown type: ' + props.type)
}
} else {
throw new Error('Unkown share type')
}
load()
async function load() {
sharables.value = await stuffService.getAll(stuffModel)
sharables.value.forEach(({id, right}) =>
selectedRight.value[id] = right,
)
}
async function deleteSharable() {
if (props.shareType === 'user') {
stuffModel.userId = sharable.value.username
} else if (props.shareType === 'team') {
stuffModel.teamId = sharable.value.id
}
await stuffService.delete(stuffModel)
showDeleteModal.value = false
for (const i in sharables.value) {
if (
(sharables.value[i].username === stuffModel.userId && props.shareType === 'user') ||
(sharables.value[i].id === stuffModel.teamId && props.shareType === 'team')
) {
sharables.value.splice(i, 1)
}
}
success({
message: t('project.share.userTeam.removeSuccess', {
type: shareTypeName.value,
sharable: sharableName.value,
}),
})
}
async function add(admin) {
if (admin === null) {
admin = false
}
stuffModel.right = RIGHTS.READ
if (admin) {
stuffModel.right = RIGHTS.ADMIN
}
if (props.shareType === 'user') {
stuffModel.userId = sharable.value.username
} else if (props.shareType === 'team') {
stuffModel.teamId = sharable.value.id
}
await stuffService.create(stuffModel)
success({message: t('project.share.userTeam.addedSuccess', {type: shareTypeName.value})})
await load()
}
async function toggleType(sharable) {
if (
selectedRight.value[sharable.id] !== RIGHTS.ADMIN &&
selectedRight.value[sharable.id] !== RIGHTS.READ &&
selectedRight.value[sharable.id] !== RIGHTS.READ_WRITE
) {
selectedRight.value[sharable.id] = RIGHTS.READ
}
stuffModel.right = selectedRight.value[sharable.id]
if (props.shareType === 'user') {
stuffModel.userId = sharable.username
} else if (props.shareType === 'team') {
stuffModel.teamId = sharable.id
}
const r = await stuffService.update(stuffModel)
for (const i in sharables.value) {
if (
(sharables.value[i].username ===
stuffModel.userId &&
props.shareType === 'user') ||
(sharables.value[i].id === stuffModel.teamId &&
props.shareType === 'team')
) {
sharables.value[i].right = r.right
}
}
success({message: t('project.share.userTeam.updatedSuccess', {type: shareTypeName.value})})
}
const found = ref([])
const currentUserId = computed(() => authStore.info.id)
async function find(query: string) {
if (query === '') {
found.value = []
return
}
const results = await searchService.getAll({}, {s: query})
found.value = results
.filter(m => {
if(props.shareType === 'user' && m.id === currentUserId.value) {
return false
}
return typeof sharables.value.find(s => s.id === m.id) === 'undefined'
})
}
</script>