mirror of
https://github.com/go-vikunja/vikunja.git
synced 2025-12-05 19:16:51 -06:00
fix: address ts errors in user and team views
This commit is contained in:
@@ -39,7 +39,7 @@ import ContentLinkShare from '@/components/home/ContentLinkShare.vue'
|
||||
import NoAuthWrapper from '@/components/misc/NoAuthWrapper.vue'
|
||||
import Ready from '@/components/misc/Ready.vue'
|
||||
|
||||
import {setLanguage} from '@/i18n'
|
||||
import {setLanguage, getBrowserLanguage} from '@/i18n'
|
||||
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
@@ -88,7 +88,7 @@ watch(userEmailConfirm, (userEmailConfirm) => {
|
||||
router.push({name: 'user.login'})
|
||||
}, { immediate: true })
|
||||
|
||||
setLanguage(authStore.settings.language)
|
||||
setLanguage(authStore.settings.language ?? getBrowserLanguage())
|
||||
useColorScheme()
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
import {logEvent} from 'histoire/client'
|
||||
import {reactive} from 'vue'
|
||||
import {reactive, type App} from 'vue'
|
||||
import {createRouter, createMemoryHistory} from 'vue-router'
|
||||
import BaseButton from './BaseButton.vue'
|
||||
|
||||
function setupApp({ app }) {
|
||||
function setupApp({ app }: { app: App }) {
|
||||
// Router mock
|
||||
app.use(createRouter({
|
||||
history: createMemoryHistory(),
|
||||
|
||||
@@ -83,47 +83,51 @@ function forceLayout(el: HTMLElement) {
|
||||
# see: https://vuejs.org/guide/built-ins/transition.html#javascript-hooks
|
||||
###################################################################### */
|
||||
|
||||
function beforeEnter(el: HTMLElement) {
|
||||
el.style.height = '0'
|
||||
el.style.willChange = 'height'
|
||||
el.style.backfaceVisibility = 'hidden'
|
||||
forceLayout(el)
|
||||
function beforeEnter(el: Element) {
|
||||
const element = el as HTMLElement
|
||||
element.style.height = '0'
|
||||
element.style.willChange = 'height'
|
||||
element.style.backfaceVisibility = 'hidden'
|
||||
forceLayout(element)
|
||||
}
|
||||
|
||||
// the done callback is optional when
|
||||
// used in combination with CSS
|
||||
function enter(el: HTMLElement) {
|
||||
const height = getHeight(el) // Get the natural height
|
||||
el.style.height = height // Update the height
|
||||
function enter(el: Element) {
|
||||
const element = el as HTMLElement
|
||||
const height = getHeight(element) // Get the natural height
|
||||
element.style.height = height // Update the height
|
||||
}
|
||||
|
||||
function afterEnter(el: HTMLElement) {
|
||||
removeHeight(el)
|
||||
function afterEnter(el: Element) {
|
||||
removeHeight(el as HTMLElement)
|
||||
}
|
||||
|
||||
function enterCancelled(el: HTMLElement) {
|
||||
removeHeight(el)
|
||||
function enterCancelled(el: Element) {
|
||||
removeHeight(el as HTMLElement)
|
||||
}
|
||||
|
||||
function beforeLeave(el: HTMLElement) {
|
||||
function beforeLeave(el: Element) {
|
||||
const element = el as HTMLElement
|
||||
// Give the element a height to change from
|
||||
el.style.height = `${el.scrollHeight}px`
|
||||
forceLayout(el)
|
||||
element.style.height = `${element.scrollHeight}px`
|
||||
forceLayout(element)
|
||||
}
|
||||
|
||||
function leave(el: HTMLElement) {
|
||||
function leave(el: Element) {
|
||||
const element = el as HTMLElement
|
||||
// Set the height back to 0
|
||||
el.style.height = '0'
|
||||
el.style.willChange = ''
|
||||
el.style.backfaceVisibility = ''
|
||||
element.style.height = '0'
|
||||
element.style.willChange = ''
|
||||
element.style.backfaceVisibility = ''
|
||||
}
|
||||
|
||||
function afterLeave(el: HTMLElement) {
|
||||
removeHeight(el)
|
||||
function afterLeave(el: Element) {
|
||||
removeHeight(el as HTMLElement)
|
||||
}
|
||||
|
||||
function leaveCancelled(el: HTMLElement) {
|
||||
removeHeight(el)
|
||||
function leaveCancelled(el: Element) {
|
||||
removeHeight(el as HTMLElement)
|
||||
}
|
||||
|
||||
function removeHeight(el: HTMLElement) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Make date objects from timestamps
|
||||
*/
|
||||
export function parseDateOrNull(date: string | Date) {
|
||||
export function parseDateOrNull(date: string | Date | null | undefined) {
|
||||
if (date instanceof Date) {
|
||||
return date
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type {IAbstract} from './IAbstract'
|
||||
|
||||
export type AvatarProvider = 'default' | 'initials' | 'gravatar' | 'marble' | 'upload'
|
||||
export type AvatarProvider = 'default' | 'initials' | 'gravatar' | 'marble' | 'upload' | 'ldap'
|
||||
|
||||
export interface IAvatar extends IAbstract {
|
||||
avatarProvider: AvatarProvider
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import type {IAbstract} from './IAbstract'
|
||||
|
||||
export interface ICaldavToken extends IAbstract {
|
||||
id: number;
|
||||
id: number;
|
||||
|
||||
created: Date;
|
||||
created: Date;
|
||||
|
||||
/**
|
||||
* The actual token value is only returned when creating a new token.
|
||||
*/
|
||||
token?: string;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface IUser extends IAbstract {
|
||||
updated: Date
|
||||
settings: IUserSettings
|
||||
|
||||
isLocalUser: boolean
|
||||
deletionScheduledAt: string | Date | null
|
||||
isLocalUser: boolean
|
||||
deletionScheduledAt: string | Date | null
|
||||
authProvider?: string
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@ export default class ApiTokenModel extends AbstractModel<IApiToken> {
|
||||
id = 0
|
||||
title = ''
|
||||
token = ''
|
||||
permissions = null
|
||||
expiresAt: Date = null
|
||||
created: Date = null
|
||||
permissions = {}
|
||||
expiresAt: Date = new Date()
|
||||
created: Date = new Date()
|
||||
|
||||
constructor(data: Partial<IApiToken> = {}) {
|
||||
super()
|
||||
|
||||
@@ -271,6 +271,7 @@ import User from '@/components/misc/User.vue'
|
||||
import TeamService from '@/services/team'
|
||||
import TeamMemberService from '@/services/teamMember'
|
||||
import UserService from '@/services/user'
|
||||
import UserModel from '@/models/user'
|
||||
|
||||
import {RIGHTS as Rights} from '@/constants/rights'
|
||||
|
||||
@@ -305,8 +306,8 @@ const userService = ref<UserService>(new UserService())
|
||||
const team = ref<ITeam>()
|
||||
const teamId = computed(() => Number(route.params.id))
|
||||
const memberToDelete = ref<ITeamMember>()
|
||||
const newMember = ref<IUser>()
|
||||
const foundUsers = ref<IUser[]>()
|
||||
const newMember = ref<IUser | null>(null)
|
||||
const foundUsers = ref<IUser[]>([])
|
||||
|
||||
const showDeleteModal = ref(false)
|
||||
const showUserDeleteModal = ref(false)
|
||||
@@ -331,22 +332,22 @@ async function save() {
|
||||
}
|
||||
showErrorTeamnameRequired.value = false
|
||||
|
||||
team.value = await teamService.value.update(team.value)
|
||||
team.value = await teamService.value.update(team.value as ITeam)
|
||||
success({message: t('team.edit.success')})
|
||||
}
|
||||
|
||||
async function deleteTeam() {
|
||||
await teamService.value.delete(team.value)
|
||||
await teamService.value.delete(team.value as ITeam)
|
||||
success({message: t('team.edit.delete.success')})
|
||||
router.push({name: 'teams.index'})
|
||||
}
|
||||
|
||||
async function deleteMember() {
|
||||
try {
|
||||
await teamMemberService.value.delete({
|
||||
teamId: teamId.value,
|
||||
username: memberToDelete.value.username,
|
||||
})
|
||||
await teamMemberService.value.delete({
|
||||
teamId: teamId.value,
|
||||
username: memberToDelete.value?.username ?? '',
|
||||
} as unknown as ITeamMember)
|
||||
success({message: t('team.edit.deleteUser.success')})
|
||||
await loadTeam()
|
||||
} finally {
|
||||
@@ -360,11 +361,11 @@ async function addUser() {
|
||||
showMustSelectUserError.value = true
|
||||
return
|
||||
}
|
||||
await teamMemberService.value.create({
|
||||
teamId: teamId.value,
|
||||
username: newMember.value.username,
|
||||
})
|
||||
newMember.value = null
|
||||
await teamMemberService.value.create({
|
||||
teamId: teamId.value,
|
||||
username: newMember.value!.username,
|
||||
} as unknown as ITeamMember)
|
||||
newMember.value = null
|
||||
await loadTeam()
|
||||
success({message: t('team.edit.userAddedSuccess')})
|
||||
}
|
||||
@@ -393,16 +394,16 @@ async function findUser(query: string) {
|
||||
return
|
||||
}
|
||||
|
||||
const users = await userService.value.getAll({}, {s: query})
|
||||
foundUsers.value = users.filter((u: IUser) => u.id !== userInfo.value.id)
|
||||
const users = await userService.value.getAll(new UserModel(), {s: query})
|
||||
foundUsers.value = users.filter((u: IUser) => u.id !== userInfo.value?.id)
|
||||
}
|
||||
|
||||
async function leave() {
|
||||
try {
|
||||
await teamMemberService.value.delete({
|
||||
teamId: teamId.value,
|
||||
username: userInfo.value.username,
|
||||
})
|
||||
await teamMemberService.value.delete({
|
||||
teamId: teamId.value,
|
||||
username: userInfo.value?.username ?? '',
|
||||
} as unknown as ITeamMember)
|
||||
success({message: t('team.edit.leave.success')})
|
||||
await router.push({name: 'home'})
|
||||
} finally {
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, shallowReactive} from 'vue'
|
||||
import type { ITeam } from '@/modelTypes/ITeam'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import TeamService from '@/services/team'
|
||||
@@ -49,7 +50,7 @@ import { useTitle } from '@/composables/useTitle'
|
||||
const { t } = useI18n({useScope: 'global'})
|
||||
useTitle(() => t('team.title'))
|
||||
|
||||
const teams = ref([])
|
||||
const teams = ref<ITeam[]>([])
|
||||
const teamService = shallowReactive(new TeamService())
|
||||
teamService.getAll().then((result) => {
|
||||
teams.value = result
|
||||
|
||||
@@ -208,16 +208,23 @@ const validateUsernameField = useDebounceFn(() => {
|
||||
const needsTotpPasscode = computed(() => authStore.needsTotpPasscode)
|
||||
const totpPasscode = ref<HTMLInputElement | null>(null)
|
||||
|
||||
interface Credentials {
|
||||
username: string | undefined
|
||||
password: string
|
||||
longToken: boolean
|
||||
totpPasscode?: string
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
errorMessage.value = ''
|
||||
errorMessage.value = ''
|
||||
// Some browsers prevent Vue bindings from working with autofilled values.
|
||||
// To work around this, we're manually getting the values here instead of relying on vue bindings.
|
||||
// For more info, see https://kolaente.dev/vikunja/frontend/issues/78
|
||||
const credentials = {
|
||||
username: usernameRef.value?.value,
|
||||
password: password.value,
|
||||
longToken: rememberMe.value,
|
||||
}
|
||||
const credentials: Credentials = {
|
||||
username: usernameRef.value?.value,
|
||||
password: password.value,
|
||||
longToken: rememberMe.value,
|
||||
}
|
||||
|
||||
if (credentials.username === '' || credentials.password === '') {
|
||||
// Trigger the validation error messages
|
||||
|
||||
@@ -30,10 +30,11 @@
|
||||
class="label"
|
||||
for="password"
|
||||
>{{ $t('user.auth.password') }}</label>
|
||||
<Password
|
||||
@submit="resetPassword"
|
||||
@update:modelValue="v => credentials.password = v"
|
||||
/>
|
||||
<Password
|
||||
:model-value="credentials.password"
|
||||
@submit="resetPassword"
|
||||
@update:modelValue="v => credentials.password = v"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field is-grouped">
|
||||
@@ -85,9 +86,9 @@ async function resetPassword() {
|
||||
}
|
||||
|
||||
const passwordReset = new PasswordResetModel({newPassword: credentials.password, token: token})
|
||||
try {
|
||||
const {message} = await passwordResetService.resetPassword(passwordReset)
|
||||
successMessage.value = message
|
||||
try {
|
||||
const {message} = await passwordResetService.resetPassword(passwordReset) as unknown as {message: string}
|
||||
successMessage.value = message
|
||||
} catch (e) {
|
||||
errorMsg.value = e.response.data.message
|
||||
}
|
||||
|
||||
@@ -72,11 +72,12 @@
|
||||
class="label"
|
||||
for="password"
|
||||
>{{ $t('user.auth.password') }}</label>
|
||||
<Password
|
||||
:validate-initially="validatePasswordInitially"
|
||||
@submit="submit"
|
||||
@update:modelValue="v => credentials.password = v"
|
||||
/>
|
||||
<Password
|
||||
:model-value="credentials.password"
|
||||
:validate-initially="validatePasswordInitially"
|
||||
@submit="submit"
|
||||
@update:modelValue="v => credentials.password = v"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<XButton
|
||||
@@ -199,8 +200,9 @@ async function submit() {
|
||||
|
||||
try {
|
||||
await authStore.register(toRaw(credentials))
|
||||
} catch (e) {
|
||||
errorMessage.value = e?.message
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
const err = e as {message?: string}
|
||||
errorMessage.value = err.message ?? ''
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -79,9 +79,10 @@ async function requestPasswordReset() {
|
||||
try {
|
||||
await passwordResetService.requestResetPassword(passwordReset.value)
|
||||
isSuccess.value = true
|
||||
} catch (e) {
|
||||
errorMsg.value = e.response.data.message
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
const err = e as {response?: {data?: {message?: string}}}
|
||||
errorMsg.value = err.response?.data?.message ?? ''
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -18,19 +18,19 @@ const service = new ApiTokenService()
|
||||
const tokens = ref<IApiToken[]>([])
|
||||
const apiDocsUrl = window.API_URL + '/docs'
|
||||
const showCreateForm = ref(false)
|
||||
const availableRoutes = ref(null)
|
||||
const availableRoutes = ref<Record<string, Record<string, string[]>> | null>(null)
|
||||
const newToken = ref<IApiToken>(new ApiTokenModel())
|
||||
const newTokenExpiry = ref<string | number>(30)
|
||||
const newTokenExpiryCustom = ref(new Date())
|
||||
const newTokenPermissions = ref({})
|
||||
const newTokenPermissionsGroup = ref({})
|
||||
const newTokenPermissions = ref<Record<string, Record<string, boolean>>>({})
|
||||
const newTokenPermissionsGroup = ref<Record<string, boolean>>({})
|
||||
const newTokenTitleValid = ref(true)
|
||||
const newTokenPermissionValid = ref(true)
|
||||
const apiTokenTitle = ref()
|
||||
const tokenCreatedSuccessMessage = ref('')
|
||||
|
||||
const showDeleteModal = ref<boolean>(false)
|
||||
const tokenToDelete = ref<IApiToken>()
|
||||
const tokenToDelete = ref<IApiToken | null>(null)
|
||||
|
||||
const {t} = useI18n()
|
||||
|
||||
@@ -50,7 +50,7 @@ onMounted(async () => {
|
||||
tokens.value = await service.getAll()
|
||||
const allRoutes = await service.getAvailableRoutes()
|
||||
|
||||
const routesAvailable = {}
|
||||
const routesAvailable: Record<string, Record<string, string[]>> = {}
|
||||
const keys = Object.keys(allRoutes)
|
||||
keys.sort((a, b) => (a === 'other' ? 1 : b === 'other' ? -1 : 0))
|
||||
keys.forEach(key => {
|
||||
@@ -63,8 +63,8 @@ onMounted(async () => {
|
||||
})
|
||||
|
||||
function resetPermissions() {
|
||||
newTokenPermissions.value = {}
|
||||
Object.entries(availableRoutes.value).forEach(entry => {
|
||||
newTokenPermissions.value = {}
|
||||
Object.entries(availableRoutes.value || {}).forEach(entry => {
|
||||
const [group, routes] = entry
|
||||
newTokenPermissions.value[group] = {}
|
||||
Object.keys(routes).forEach(r => {
|
||||
@@ -74,13 +74,16 @@ function resetPermissions() {
|
||||
}
|
||||
|
||||
async function deleteToken() {
|
||||
await service.delete(tokenToDelete.value)
|
||||
showDeleteModal.value = false
|
||||
const index = tokens.value.findIndex(el => el.id === tokenToDelete.value.id)
|
||||
tokenToDelete.value = null
|
||||
if (index === -1) {
|
||||
return
|
||||
}
|
||||
if (!tokenToDelete.value) {
|
||||
return
|
||||
}
|
||||
await service.delete(tokenToDelete.value!)
|
||||
showDeleteModal.value = false
|
||||
const index = tokens.value.findIndex(el => el.id === tokenToDelete.value!.id)
|
||||
tokenToDelete.value = null
|
||||
if (index === -1) {
|
||||
return
|
||||
}
|
||||
tokens.value.splice(index, 1)
|
||||
}
|
||||
|
||||
@@ -127,22 +130,22 @@ async function createToken() {
|
||||
showCreateForm.value = false
|
||||
}
|
||||
|
||||
function formatPermissionTitle(title: string): string {
|
||||
return title.replaceAll('_', ' ')
|
||||
function formatPermissionTitle(title: string | number): string {
|
||||
return String(title).split('_').join(' ')
|
||||
}
|
||||
|
||||
function selectPermissionGroup(group: string, checked: boolean) {
|
||||
Object.entries(availableRoutes.value[group]).forEach(entry => {
|
||||
function selectPermissionGroup(group: string | number, checked: boolean) {
|
||||
Object.entries(availableRoutes.value?.[group] || {}).forEach(entry => {
|
||||
const [key] = entry
|
||||
newTokenPermissions.value[group][key] = checked
|
||||
})
|
||||
}
|
||||
|
||||
function toggleGroupPermissionsFromChild(group: string, checked: boolean) {
|
||||
if (checked) {
|
||||
// Check if all permissions of that group are checked and check the "select all" checkbox in that case
|
||||
let allChecked = true
|
||||
Object.entries(availableRoutes.value[group]).forEach(entry => {
|
||||
function toggleGroupPermissionsFromChild(group: string | number, checked: boolean) {
|
||||
if (checked) {
|
||||
// Check if all permissions of that group are checked and check the "select all" checkbox in that case
|
||||
let allChecked = true
|
||||
Object.entries(availableRoutes.value?.[group] || {}).forEach(entry => {
|
||||
const [key] = entry
|
||||
if (!newTokenPermissions.value[group][key]) {
|
||||
allChecked = false
|
||||
@@ -368,7 +371,7 @@ function toggleGroupPermissionsFromChild(group: string, checked: boolean) {
|
||||
|
||||
<template #text>
|
||||
<p>
|
||||
{{ $t('user.settings.apiTokens.delete.text1', {token: tokenToDelete.title}) }}<br>
|
||||
{{ $t('user.settings.apiTokens.delete.text1', {token: tokenToDelete?.title}) }}<br>
|
||||
{{ $t('user.settings.apiTokens.delete.text2') }}
|
||||
</p>
|
||||
</template>
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<XButton
|
||||
v-if="!isCropAvatar"
|
||||
:loading="avatarService.loading || loading"
|
||||
@click="avatarUploadInput.click()"
|
||||
@click="avatarUploadInput?.click()"
|
||||
>
|
||||
{{ $t('user.settings.avatar.uploadAvatar') }}
|
||||
</XButton>
|
||||
@@ -87,6 +87,7 @@ import {useTitle} from '@/composables/useTitle'
|
||||
import {success} from '@/message'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import Message from '@/components/misc/Message.vue'
|
||||
import type { AvatarProvider } from '@/modelTypes/IAvatar'
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
const authStore = useAuthStore()
|
||||
@@ -106,11 +107,11 @@ const avatarService = shallowReactive(new AvatarService())
|
||||
const loading = ref(false)
|
||||
|
||||
|
||||
const avatarProvider = ref('')
|
||||
const avatarProvider = ref<AvatarProvider>('default')
|
||||
|
||||
async function avatarStatus() {
|
||||
const {avatarProvider: currentProvider} = await avatarService.get({})
|
||||
avatarProvider.value = currentProvider
|
||||
const {avatarProvider: currentProvider} = await avatarService.get(new AvatarModel({}))
|
||||
avatarProvider.value = currentProvider
|
||||
}
|
||||
|
||||
avatarStatus()
|
||||
@@ -134,9 +135,11 @@ async function uploadAvatar() {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const blob = await new Promise(resolve => canvas.toBlob(blob => resolve(blob)))
|
||||
await avatarService.create(blob)
|
||||
try {
|
||||
const blob = await new Promise<Blob | null>(resolve => canvas.toBlob((b: Blob | null) => resolve(b)))
|
||||
if (blob) {
|
||||
await avatarService.create(blob)
|
||||
}
|
||||
success({message: t('user.settings.avatar.setSuccess')})
|
||||
authStore.reloadAvatar()
|
||||
} finally {
|
||||
@@ -145,24 +148,26 @@ async function uploadAvatar() {
|
||||
}
|
||||
}
|
||||
|
||||
const avatarToCrop = ref()
|
||||
const avatarUploadInput = ref()
|
||||
const avatarToCrop = ref<string | ArrayBuffer | null>(null)
|
||||
const avatarUploadInput = ref<HTMLInputElement | null>(null)
|
||||
|
||||
function cropAvatar() {
|
||||
const avatar = avatarUploadInput.value.files
|
||||
const files = avatarUploadInput.value?.files
|
||||
|
||||
if (avatar.length === 0) {
|
||||
return
|
||||
}
|
||||
if (!files || files.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
const reader = new FileReader()
|
||||
reader.onload = e => {
|
||||
avatarToCrop.value = e.target.result
|
||||
isCropAvatar.value = true
|
||||
}
|
||||
const reader = new FileReader()
|
||||
reader.onload = e => {
|
||||
const target = e.target as FileReader | null
|
||||
if (!target) return
|
||||
avatarToCrop.value = target.result
|
||||
isCropAvatar.value = true
|
||||
}
|
||||
reader.onloadend = () => loading.value = false
|
||||
reader.readAsDataURL(avatar[0])
|
||||
reader.readAsDataURL(files[0])
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -109,6 +109,7 @@ import {success} from '@/message'
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import Message from '@/components/misc/Message.vue'
|
||||
import CaldavTokenService from '@/services/caldavToken'
|
||||
import CaldavTokenModel from '@/models/caldavToken'
|
||||
import { formatDateShort } from '@/helpers/time/formatDate'
|
||||
import type {ICaldavToken} from '@/modelTypes/ICaldavToken'
|
||||
import {useConfigStore} from '@/stores/config'
|
||||
@@ -128,8 +129,10 @@ service.getAll().then((result: ICaldavToken[]) => {
|
||||
|
||||
const newToken = ref<ICaldavToken>()
|
||||
async function createToken() {
|
||||
newToken.value = await service.create({}) as ICaldavToken
|
||||
tokens.value.push(newToken.value)
|
||||
// The API does not require any payload when creating a token.
|
||||
// Create an empty model instance to satisfy the expected type.
|
||||
newToken.value = await service.create(new CaldavTokenModel({}))
|
||||
tokens.value.push(newToken.value)
|
||||
}
|
||||
|
||||
async function deleteToken(token: ICaldavToken) {
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
v-if="isExternalUser"
|
||||
class="help"
|
||||
>
|
||||
{{ $t('user.settings.general.externalUserNameChange', {provider: authStore.info.authProvider}) }}
|
||||
{{ $t('user.settings.general.externalUserNameChange', {provider: authStore.info?.authProvider}) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
@@ -252,7 +252,7 @@ export default {name: 'UserSettingsGeneral'}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed, watch, ref} from 'vue'
|
||||
import {computed, watch, ref, type Ref} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
import {PrefixMode} from '@/modules/parseTaskText'
|
||||
@@ -271,6 +271,8 @@ import {useAuthStore} from '@/stores/auth'
|
||||
import type {IUserSettings} from '@/modelTypes/IUserSettings'
|
||||
import {isSavedFilter} from '@/services/savedFilter'
|
||||
import {DEFAULT_PROJECT_VIEW_SETTINGS} from '@/modelTypes/IProjectView'
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
import ProjectModel from '@/models/project'
|
||||
import {PRIORITIES} from '@/constants/priorities'
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
@@ -309,9 +311,9 @@ function useAvailableTimezones(settingsRef: Ref<IUserSettings>) {
|
||||
.then(r => {
|
||||
if (r.data) {
|
||||
// Transform timezones into objects with value/label pairs
|
||||
availableTimezones.value = r.data
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.map((tz: string) => ({
|
||||
availableTimezones.value = r.data
|
||||
.sort((a: string, b: string) => a.localeCompare(b))
|
||||
.map((tz: string) => ({
|
||||
value: tz,
|
||||
label: tz.replace(/_/g, ' '),
|
||||
}))
|
||||
@@ -364,12 +366,12 @@ const {
|
||||
|
||||
const id = ref(createRandomID())
|
||||
const availableLanguageOptions = ref(
|
||||
Object.entries(SUPPORTED_LOCALES)
|
||||
.map(l => ({code: l[0], title: l[1]}))
|
||||
.sort((a, b) => a.title.localeCompare(b.title)),
|
||||
Object.entries(SUPPORTED_LOCALES)
|
||||
.map(l => ({code: l[0], title: l[1]}))
|
||||
.sort((a: {code: string; title: string}, b: {code: string; title: string}) => a.title.localeCompare(b.title)),
|
||||
)
|
||||
|
||||
const isExternalUser = computed(() => !authStore.info.isLocalUser)
|
||||
const isExternalUser = computed(() => !authStore.info?.isLocalUser)
|
||||
|
||||
watch(
|
||||
() => authStore.settings,
|
||||
@@ -384,25 +386,30 @@ watch(
|
||||
)
|
||||
|
||||
const projectStore = useProjectStore()
|
||||
const defaultProject = computed({
|
||||
get: () => projectStore.projects[settings.value.defaultProjectId],
|
||||
set(l) {
|
||||
settings.value.defaultProjectId = l ? l.id : DEFAULT_PROJECT_ID
|
||||
},
|
||||
const defaultProject = computed<IProject>({
|
||||
get: () => settings.value.defaultProjectId !== undefined
|
||||
? (projectStore.projects[settings.value.defaultProjectId] as IProject) ?? new ProjectModel()
|
||||
: new ProjectModel(),
|
||||
set(l: IProject) {
|
||||
settings.value.defaultProjectId = l ? l.id : DEFAULT_PROJECT_ID
|
||||
},
|
||||
})
|
||||
const filterUsedInOverview = computed({
|
||||
get: () => projectStore.projects[settings.value.frontendSettings.filterIdUsedOnOverview],
|
||||
set(l) {
|
||||
settings.value.frontendSettings.filterIdUsedOnOverview = l ? l.id : null
|
||||
},
|
||||
const filterUsedInOverview = computed<IProject>({
|
||||
get: () => settings.value.frontendSettings.filterIdUsedOnOverview !== null
|
||||
? (projectStore.projects[settings.value.frontendSettings.filterIdUsedOnOverview] as IProject) ?? new ProjectModel()
|
||||
: new ProjectModel(),
|
||||
set(l: IProject) {
|
||||
settings.value.frontendSettings.filterIdUsedOnOverview = l ? l.id : null
|
||||
},
|
||||
})
|
||||
const hasFilters = computed(() => typeof projectStore.projectsArray.find(p => isSavedFilter(p)) !== 'undefined')
|
||||
const hasFilters = computed(() => typeof projectStore.projectsArray.find(p => isSavedFilter(p as IProject)) !== 'undefined')
|
||||
const loading = computed(() => authStore.isLoadingGeneralSettings)
|
||||
|
||||
async function updateSettings() {
|
||||
await authStore.saveUserSettings({
|
||||
settings: {...settings.value},
|
||||
})
|
||||
await authStore.saveUserSettings({
|
||||
settings: {...settings.value},
|
||||
showMessage: true,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -135,7 +135,7 @@ async function totpStatus() {
|
||||
return
|
||||
}
|
||||
try {
|
||||
totp.value = await totpService.get({})
|
||||
totp.value = await totpService.get(new TotpModel())
|
||||
totpSetQrCode()
|
||||
} catch(e: unknown) {
|
||||
// Error code 1016 means totp is not enabled, we don't need an error in that case.
|
||||
|
||||
@@ -20,7 +20,8 @@
|
||||
"strictNullChecks": true,
|
||||
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
"@/*": ["./src/*"],
|
||||
"vite-plugin-sentry/client": ["./node_modules/vite-plugin-sentry/client.d.ts"]
|
||||
},
|
||||
"types": [
|
||||
// https://github.com/ikenfin/vite-plugin-sentry#typescript
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/// <reference types="vitest" />
|
||||
import {defineConfig, type PluginOption, loadEnv} from 'vite'
|
||||
import {defineConfig, type PluginOption, loadEnv, type ImportMetaEnv} from 'vite'
|
||||
import {configDefaults} from 'vitest/config'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import {URL, fileURLToPath} from 'node:url'
|
||||
|
||||
Reference in New Issue
Block a user