fix(front): make (`lint:tsc`) completely happy
This commit is contained in:
		
							parent
							
								
									d8a7c033eb
								
							
						
					
					
						commit
						1972f6c8a0
					
				|  | @ -18,7 +18,7 @@ | |||
|     "test:generate-mock-server": "msw-auto-mock ../docs/schema.yml -o test/msw-server.ts --node", | ||||
|     "lint": "yarn lint:es && yarn lint:tsc", | ||||
|     "lint:es": "eslint --max-warnings 0 --cache --cache-strategy content --ext .ts,.js,.vue,.json,.html,.cjs . cypress public/embed.html src test ui-docs", | ||||
|     "lint:tsc": "vue-tsc --noEmit --incremental && tsc --noEmit --incremental -p cypress src test ui-docs", | ||||
|     "lint:tsc": "vue-tsc --noEmit --incremental && tsc --noEmit --incremental --project tsconfig.json", | ||||
|     "generate-types-from-local-schema": "yarn run openapi-typescript ../api/funkwhale_api/common/schema.yml -o src/generated/types.ts", | ||||
|     "generate-types-from-remote-schema": "yarn run openapi-typescript https://docs.funkwhale.audio/develop/swagger/schema.yml -o src/generated/types.ts", | ||||
|     "fmt:es": "yarn lint:es --fix", | ||||
|  |  | |||
|  | @ -108,7 +108,7 @@ watch( | |||
|       /> | ||||
|     </slot> | ||||
|     <Pagination | ||||
|       v-if="albums && count > props.limit" | ||||
|       v-if="page && albums && count > props.limit" | ||||
|       v-model:page="page" | ||||
|       :pages="Math.ceil((count || 0) / props.limit)" | ||||
|       style="grid-column: 1 / -1;" | ||||
|  |  | |||
|  | @ -108,7 +108,7 @@ watch( | |||
|       :artist="artist" | ||||
|     /> | ||||
|     <Pagination | ||||
|       v-if="artists && count > limit" | ||||
|       v-if="page && artists && count > limit" | ||||
|       v-model:page="page" | ||||
|       style="grid-column: 1 / -1;" | ||||
|       :pages="Math.ceil((count || 0) / limit)" | ||||
|  |  | |||
|  | @ -90,7 +90,7 @@ watch([() => props.filters, page], | |||
|       /> | ||||
|     </template> | ||||
|     <Pagination | ||||
|       v-if="result && count > limit && limit > 16" | ||||
|       v-if="page && result && count > limit && limit > 16" | ||||
|       v-model:page="page" | ||||
|       :pages="Math.ceil((count || 0) / limit)" | ||||
|       style="grid-column: 1 / -1;" | ||||
|  | @ -101,7 +101,7 @@ watch([() => props.filters, page], | |||
|       :object="channel" | ||||
|     /> | ||||
|     <Pagination | ||||
|       v-if="result && count > limit" | ||||
|       v-if="page && result && count > limit" | ||||
|       v-model:page="page" | ||||
|       :pages="Math.ceil((count || 0) / limit)" | ||||
|       style="grid-column: 1 / -1;" | ||||
|  |  | |||
|  | @ -216,7 +216,7 @@ watch(() => props.websocketHandlers.includes('Listen'), (to) => { | |||
|       /> | ||||
|     </div> | ||||
|     <Pagination | ||||
|       v-if="count > props.limit" | ||||
|       v-if="page && count > props.limit" | ||||
|       v-model:page="page" | ||||
|       :pages="Math.ceil((count || 0) / props.limit)" | ||||
|       style="grid-column: 1 / -1;" | ||||
|  |  | |||
|  | @ -215,7 +215,7 @@ const paginateOptions = computed(() => sortedUniq([12, 25, 50, paginateBy.value] | |||
|         </Layout> | ||||
|       </Layout> | ||||
|       <Pagination | ||||
|         v-if="results && count > paginateBy" | ||||
|         v-if="page && results && count > paginateBy" | ||||
|         v-model:page="page" | ||||
|         :pages="Math.ceil((count || 0) / paginateBy)" | ||||
|         style="grid-column: 1 / -1;" | ||||
|  |  | |||
|  | @ -229,7 +229,7 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value] | |||
|     </Layout> | ||||
|     <Loader v-if="isLoading" /> | ||||
|     <Pagination | ||||
|       v-if="result && result.count > paginateBy" | ||||
|       v-if="page && result && result.count > paginateBy" | ||||
|       v-model:page="page" | ||||
|       :pages="Math.ceil((result.count || 0)/paginateBy)" | ||||
|     /> | ||||
|  | @ -271,7 +271,7 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value] | |||
|     </Layout> | ||||
|     <Spacer grow /> | ||||
|     <Pagination | ||||
|       v-if="result && result.count > paginateBy" | ||||
|       v-if="page && result && result.count > paginateBy" | ||||
|       v-model:page="page" | ||||
|       :pages="Math.ceil((result.count || 0)/paginateBy)" | ||||
|     /> | ||||
|  |  | |||
|  | @ -237,7 +237,7 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value] | |||
|     </Layout> | ||||
|     <Loader v-if="isLoading" /> | ||||
|     <Pagination | ||||
|       v-if="result && result.count > paginateBy" | ||||
|       v-if="page && result && result.count > paginateBy" | ||||
|       v-model:page="page" | ||||
|       :pages="Math.ceil(result.count / paginateBy)" | ||||
|     /> | ||||
|  | @ -279,7 +279,7 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value] | |||
|     </Layout> | ||||
|     <Spacer grow /> | ||||
|     <Pagination | ||||
|       v-if="result && result.count > paginateBy" | ||||
|       v-if="page && result && result.count > paginateBy" | ||||
|       v-model:page="page" | ||||
|       :pages="Math.ceil(result.count / paginateBy)" | ||||
|     /> | ||||
|  |  | |||
|  | @ -309,7 +309,7 @@ const { to: upload } = useModal('upload') | |||
|     </Layout> | ||||
|     <Spacer grow /> | ||||
|     <Pagination | ||||
|       v-if="result && result.count > paginateBy" | ||||
|       v-if="page && result && result.count > paginateBy" | ||||
|       :page="page" | ||||
|       :pages="Math.ceil((result?.results.length || 0)/paginateBy)" | ||||
|     /> | ||||
|  |  | |||
|  | @ -277,7 +277,7 @@ const paginateOptions = computed(() => sortedUniq([12, 25, 50, paginateBy.value] | |||
|       flex | ||||
|     > | ||||
|       <Pagination | ||||
|         v-if="result && result.count > paginateBy" | ||||
|         v-if="page && result && result.count > paginateBy" | ||||
|         v-model:page="page" | ||||
|         :pages="Math.ceil(result.count / paginateBy)" | ||||
|       /> | ||||
|  | @ -288,7 +288,7 @@ const paginateOptions = computed(() => sortedUniq([12, 25, 50, paginateBy.value] | |||
|         :custom-radio="radio" | ||||
|       /> | ||||
|       <Pagination | ||||
|         v-if="result && result.count > paginateBy" | ||||
|         v-if="page && result && result.count > paginateBy" | ||||
|         v-model:page="page" | ||||
|         :pages="Math.ceil(result.count / paginateBy)" | ||||
|       /> | ||||
|  |  | |||
|  | @ -250,12 +250,12 @@ const labels = computed(() => ({ | |||
|   </action-table> | ||||
|   <div> | ||||
|     <Pagination | ||||
|       v-if="result && result.count > paginateBy" | ||||
|       v-if="page && result && result.count > paginateBy" | ||||
|       v-model:page="page" | ||||
|       :pages="Math.ceil(result.count / paginateBy)" | ||||
|     /> | ||||
| 
 | ||||
|     <span v-if="result && result.results.length > 0"> | ||||
|     <span v-if="page && result && result.results.length > 0"> | ||||
|       {{ t('components.manage.ChannelsTable.pagination.results', { start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count }) }} | ||||
|     </span> | ||||
|   </div> | ||||
|  |  | |||
|  | @ -253,12 +253,12 @@ const labels = computed(() => ({ | |||
|     </template> | ||||
|   </action-table> | ||||
|   <Pagination | ||||
|     v-if="result && result.count > paginateBy" | ||||
|     v-if="page && page !== undefined && result && result.count > paginateBy" | ||||
|     v-model:page="page" | ||||
|     :pages="Math.ceil(result.count / paginateBy)" | ||||
|   /> | ||||
| 
 | ||||
|   <span v-if="result && result.results.length > 0"> | ||||
|   <span v-if="page && result && result.results.length > 0"> | ||||
|     {{ t('components.manage.library.AlbumsTable.pagination.results', {start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}) }} | ||||
|   </span> | ||||
| </template> | ||||
|  |  | |||
|  | @ -249,12 +249,12 @@ const getUrl = (artist: { channel?: number; id: number }) => { | |||
|     </template> | ||||
|   </action-table> | ||||
|   <Pagination | ||||
|     v-if="result && result.count > paginateBy" | ||||
|     v-if="page && result && result.count > paginateBy" | ||||
|     v-model:page="page" | ||||
|     :pages="Math.ceil(result.count / paginateBy)" | ||||
|   /> | ||||
| 
 | ||||
|   <span v-if="result && result.results.length > 0"> | ||||
|   <span v-if="page && result && result.results.length > 0"> | ||||
|     {{ t('components.manage.library.ArtistsTable.pagination.results', { start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count }) }} | ||||
|   </span> | ||||
| </template> | ||||
|  |  | |||
|  | @ -266,12 +266,12 @@ const getCurrentState = (target?: StateTarget): ReviewState => { | |||
|     @refresh="fetchData()" | ||||
|   /> | ||||
|   <Pagination | ||||
|     v-if="result && result.count > paginateBy" | ||||
|     v-if="page && result && result.count > paginateBy" | ||||
|     v-model:page="page" | ||||
|     :pages="Math.ceil(result.count / paginateBy)" | ||||
|   /> | ||||
| 
 | ||||
|   <span v-if="result && result.results.length > 0"> | ||||
|   <span v-if="page && result && result.results.length > 0"> | ||||
|     {{ t('components.manage.library.EditsCardList.pagination.results', { start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count }) }} | ||||
|   </span> | ||||
| </template> | ||||
|  |  | |||
|  | @ -273,12 +273,12 @@ const getPrivacyLevelChoice = (privacyLevel: PrivacyLevel) => { | |||
|     </template> | ||||
|   </action-table> | ||||
|   <Pagination | ||||
|     v-if="result && result.count > paginateBy" | ||||
|     v-if="page && result && result.count > paginateBy" | ||||
|     v-model:page="page" | ||||
|     :pages="Math.ceil(result.count / paginateBy)" | ||||
|   /> | ||||
| 
 | ||||
|   <span v-if="result && result.results.length > 0"> | ||||
|   <span v-if="page && result && result.results.length > 0"> | ||||
|     {{ t('components.manage.library.LibrariesTable.pagination.results', { start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count }) }} | ||||
|   </span> | ||||
| </template> | ||||
|  |  | |||
|  | @ -215,12 +215,12 @@ const showUploadDetailModal = ref(false) | |||
|     </template> | ||||
|   </action-table> | ||||
|   <Pagination | ||||
|     v-if="result && result.count > paginateBy" | ||||
|     v-if="page && result && result.count > paginateBy" | ||||
|     v-model:page="page" | ||||
|     :pages="Math.ceil(result.count / paginateBy)" | ||||
|   /> | ||||
| 
 | ||||
|   <span v-if="result && result.results.length > 0"> | ||||
|   <span v-if="page && result && result.results.length > 0"> | ||||
|     {{ t('components.manage.library.TagsTable.pagination.results', { start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count }) }} | ||||
|   </span> | ||||
| </template> | ||||
|  |  | |||
|  | @ -254,12 +254,12 @@ const labels = computed(() => ({ | |||
|   </action-table> | ||||
|   <div> | ||||
|     <Pagination | ||||
|       v-if="result && result.count > paginateBy" | ||||
|       v-if="page && result && result.count > paginateBy" | ||||
|       v-model:page="page" | ||||
|       :pages="Math.ceil(result.count / paginateBy)" | ||||
|     /> | ||||
| 
 | ||||
|     <span v-if="result && result.results.length > 0"> | ||||
|     <span v-if="page && result && result.results.length > 0"> | ||||
|       {{ t('components.manage.library.TracksTable.pagination.results', { start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count }) }} | ||||
|     </span> | ||||
|   </div> | ||||
|  |  | |||
|  | @ -365,12 +365,12 @@ const getPrivacyLevelChoice = (privacyLevel: PrivacyLevel) => { | |||
|     </template> | ||||
|   </action-table> | ||||
|   <Pagination | ||||
|     v-if="result && result.count > paginateBy" | ||||
|     v-if="page && result && result.count > paginateBy" | ||||
|     v-model:page="page" | ||||
|     :pages="Math.ceil(result.count / paginateBy)" | ||||
|   /> | ||||
| 
 | ||||
|   <span v-if="result && result.results.length > 0"> | ||||
|   <span v-if="page && result && result.results.length > 0"> | ||||
|     {{ t('components.manage.library.UploadsTable.pagination.results', { start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count }) }} | ||||
|   </span> | ||||
| </template> | ||||
|  |  | |||
|  | @ -231,13 +231,13 @@ const labels = computed(() => ({ | |||
|   </div> | ||||
|   <div> | ||||
|     <Pagination | ||||
|       v-if="result && result.count > paginateBy" | ||||
|       v-if="page && result && result.count > paginateBy" | ||||
|       v-model:page="page" | ||||
|       :paginate-by="paginateBy" | ||||
|       :pages="result.count" | ||||
|     /> | ||||
| 
 | ||||
|     <span v-if="result && result.results.length > 0"> | ||||
|     <span v-if="page && result && result.results.length > 0"> | ||||
|       {{ t('components.manage.moderation.AccountsTable.pagination.results', { start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count }) }} | ||||
|     </span> | ||||
|   </div> | ||||
|  |  | |||
|  | @ -262,15 +262,15 @@ const labels = computed(() => ({ | |||
|   </div> | ||||
|   <div> | ||||
|     <pagination | ||||
|       v-if="result && result.count > paginateBy" | ||||
|       v-if="page && result && result.count > paginateBy" | ||||
|       v-model:page="page" | ||||
|       v-model:pages="result.count" | ||||
|       :pages="result.count" | ||||
|       :compact="true" | ||||
|       :paginate-by="paginateBy" | ||||
|       :total="result.count" | ||||
|     /> | ||||
| 
 | ||||
|     <span v-if="result && result.results.length > 0"> | ||||
|     <span v-if="page && result && result.results.length > 0"> | ||||
|       {{ t('components.manage.moderation.DomainsTable.pagination.results', { start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count }) }} | ||||
|     </span> | ||||
|   </div> | ||||
|  |  | |||
|  | @ -230,13 +230,13 @@ const labels = computed(() => ({ | |||
|   </div> | ||||
|   <div> | ||||
|     <Pagination | ||||
|       v-if="result && result.count > paginateBy" | ||||
|       v-if="page && result && result.count > paginateBy" | ||||
|       v-model:page="page" | ||||
|       v-model:pages="result.count" | ||||
|       :paginate-by="paginateBy" | ||||
|     /> | ||||
| 
 | ||||
|     <span v-if="result && result.results.length > 0"> | ||||
|     <span v-if="page && result && result.results.length > 0"> | ||||
|       {{ t('components.manage.users.InvitationsTable.pagination.results', { start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count }, result.results.length) }} | ||||
|     </span> | ||||
|   </div> | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| <script setup lang="ts"> | ||||
| import type { Notification, LibraryFollow } from '~/types' | ||||
| import type { components } from '~/types/generated' | ||||
| import type { components } from '~/generated/types' | ||||
| import type { RouteLocationRaw } from 'vue-router' | ||||
| 
 | ||||
| import { computed, ref, watchEffect, watch } from 'vue' | ||||
| import { useI18n } from 'vue-i18n' | ||||
|  | @ -72,7 +73,10 @@ const notificationData = computed(() => { | |||
|       if (activity.related_object?.approved === null) { | ||||
|         return { | ||||
|           detailUrl, | ||||
|           message: t('components.notifications.NotificationRow.message.userPendingFollow', { username: username.value, user: activity.object.target?.full_username }), | ||||
|           message: t('components.notifications.NotificationRow.message.userPendingFollow', { username: username.value, | ||||
|             // TODO: This is just wrong. Start with fixing the types upstream. | ||||
|             // @ts-expect-error `activity.object needs to have a type. Where is it declared? | ||||
|             user: activity.object.target?.full_username }), | ||||
|           acceptFollow: { | ||||
|             buttonClass: 'success', | ||||
|             icon: 'check', | ||||
|  | @ -134,24 +138,37 @@ const handleAction = (handler?: () => void) => { | |||
| 
 | ||||
| const approveLibraryFollow = async (follow: LibraryFollow) => { | ||||
|   await axios.post(`federation/follows/library/${follow.uuid}/accept/`) | ||||
|   // TODO: This is not how Axios works. You have to send a request with | ||||
|   // the correct type as a parameter. | ||||
|   // @ts-expect-error Post this with the axios payload: { ...follow, approved: true} | ||||
|   follow.approved = true | ||||
|   item.value.is_read = true | ||||
| } | ||||
| 
 | ||||
| const rejectLibraryFollow = async (follow: LibraryFollow) => { | ||||
|   await axios.post(`federation/follows/library/${follow.uuid}/reject/`) | ||||
|   // TODO: This is not how Axios works. You have to send a request with | ||||
|   // the correct type as a parameter. | ||||
|   // @ts-expect-error Post this with the axios payload: { ...follow, approved: false} | ||||
|   follow.approved = false | ||||
|   item.value.is_read = true | ||||
| } | ||||
| 
 | ||||
| const approveUserFollow = async (follow: UserFollow) => { | ||||
| const approveUserFollow = async (follow: components["schemas"]["Follow"]) => { | ||||
|   await axios.post(`federation/follows/user/${follow.uuid}/accept/`) | ||||
|   // TODO: This is not how Axios works. You have to send a request with | ||||
|   // the correct type as a parameter. | ||||
|   // @ts-expect-error Post this with the axios payload: { ...follow, approved: true} | ||||
|   follow.approved = true | ||||
|   item.value.is_read = true | ||||
| } | ||||
| 
 | ||||
| const rejectUserFollow = async (follow: UserFollow) => { | ||||
| const rejectUserFollow = async (follow: components["schemas"]["Follow"]) => { | ||||
|   await axios.post(`federation/follows/user/${follow.uuid}/reject/`) | ||||
| 
 | ||||
|   // TODO: This is not how Axios works. You have to send a request with | ||||
|   // the correct type as a parameter. | ||||
|   // @ts-expect-error Post this with the axios payload: { ...follow, approved: false} | ||||
|   follow.approved = false | ||||
|   item.value.is_read = true | ||||
| } | ||||
|  | @ -166,11 +183,13 @@ const rejectUserFollow = async (follow: UserFollow) => { | |||
|       /> | ||||
|     </td> | ||||
|     <td> | ||||
|       <!-- TODO: Make sure `notificationData.detailUrl` has a type that satisfies `RouteLocationRaw` --> | ||||
|       <!-- @vue-ignore --> | ||||
|       <router-link | ||||
|         v-if="notificationData.detailUrl" | ||||
|         v-slot="{ navigate }" | ||||
|         custom | ||||
|         :to="notificationData.detailUrl" | ||||
|         :to="notificationData.detailUrl as RouteLocationRaw" | ||||
|       > | ||||
|         <sanitized-html | ||||
|           tag="span" | ||||
|  |  | |||
|  | @ -6,7 +6,6 @@ import defaultCover from '~/assets/audio/default-cover.png' | |||
| import { momentFormat } from '~/utils/filters' | ||||
| import { ref, computed } from 'vue' | ||||
| import { useStore } from '~/store' | ||||
| import { useRouter } from 'vue-router' | ||||
| import { useI18n } from 'vue-i18n' | ||||
| 
 | ||||
| import moment from 'moment' | ||||
|  | @ -21,7 +20,6 @@ interface Props { | |||
| 
 | ||||
| const props = defineProps<Props>() | ||||
| const store = useStore() | ||||
| const router = useRouter() | ||||
| const { t } = useI18n() | ||||
| 
 | ||||
| const images = computed(() => { | ||||
|  |  | |||
|  | @ -46,7 +46,7 @@ interface ModifiedPlaylistTrack extends PlaylistTrack { | |||
| } | ||||
| 
 | ||||
| const tracks = computed({ | ||||
|   get: () => playlistTracks.value.map((playlistTrack, index) => ({ ...playlistTrack, _id: `${index}-${playlistTrack.track.id}` } as ModifiedPlaylistTrack)), | ||||
|   get: () => playlistTracks.value.map((playlistTrack, index) => ({ ...playlistTrack, _id: `${ index }-${ playlistTrack.track }` } as ModifiedPlaylistTrack)), | ||||
|   set: (playlist) => { | ||||
|     playlistTracks.value = playlist.map((modifiedPlaylistTrack, index) => { | ||||
|       const res = { ...modifiedPlaylistTrack, index } as ModifiedPlaylistTrack | ||||
|  |  | |||
|  | @ -105,7 +105,7 @@ watch( | |||
|     /> | ||||
|   </Section> | ||||
|   <Pagination | ||||
|     v-if="objects && count > (props.filters.limit as number)" | ||||
|     v-if="page && objects && count > (props.filters.limit as number)" | ||||
|     v-model:page="page" | ||||
|     :pages="Math.ceil((count || 0) / (props.filters.limit as number))" | ||||
|   /> | ||||
|  |  | |||
|  | @ -1,6 +1,5 @@ | |||
| <script setup lang="ts"> | ||||
| import type { ComponentProps } from 'vue-component-type-helpers' | ||||
| import { useAttrs } from 'vue' | ||||
| 
 | ||||
| import Layout from '~/components/ui/Layout.vue' | ||||
| import Spacer from '~/components/ui/Spacer.vue' | ||||
|  | @ -60,6 +59,9 @@ const props = defineProps<{ | |||
|           /> | ||||
|         </div> | ||||
|         <slot name="topleft" /> | ||||
|         <!-- The inferred type of props occasionally overloads the typescript compiler. --> | ||||
|         <!-- TODO: Remove @vue-ignore once tsc is re-implemented in Go (and 10x faster) --> | ||||
|         <!-- @vue-ignore --> | ||||
|         <Heading | ||||
|           v-bind="props" | ||||
|           style=" | ||||
|  |  | |||
|  | @ -3,7 +3,6 @@ import { useElementSize } from '@vueuse/core' | |||
| import { useI18n } from 'vue-i18n' | ||||
| import { ref, computed, watch } from 'vue' | ||||
| import { isMobileView } from '~/composables/screen' | ||||
| import { preventNonNumeric } from '~/utils/event-validators' | ||||
| 
 | ||||
| import Button from '~/components/ui/Button.vue' | ||||
| import Input from '~/components/ui/Input.vue' | ||||
|  |  | |||
|  | @ -109,13 +109,23 @@ onMounted(() => { | |||
|           :key="key" | ||||
|           :class="$style.description" | ||||
|           :style="`margin-right: -20%; --current-step: 0; color: magenta;`" | ||||
|         ><Markdown :md="options[key]" /></span> | ||||
|         > | ||||
|           <!-- For some reason, the linter complains that (Record<T, string>)[T] is not string... --> | ||||
|           <!-- TODO: https://dev.funkwhale.audio/funkwhale/funkwhale/-/issues/2437 --> | ||||
|           <!-- @vue-ignore --> | ||||
|           <Markdown :md="options[model]" /> | ||||
|         </span> | ||||
|       </span> | ||||
|       <span | ||||
|         v-if="model !== undefined" | ||||
|         style="position: absolute;" | ||||
|         :class="$style.description" | ||||
|       ><Markdown :md="options[model]" /></span> | ||||
|       > | ||||
|         <!-- For some reason, the linter complains that (Record<T, string>)[T] is not string... --> | ||||
|         <!-- TODO: https://dev.funkwhale.audio/funkwhale/funkwhale/-/issues/2437 --> | ||||
|         <!-- @vue-ignore --> | ||||
|         <Markdown :md="options[model]" /> | ||||
|       </span> | ||||
|     </span> | ||||
|   </Layout> | ||||
| </template> | ||||
|  |  | |||
|  | @ -1,16 +1,9 @@ | |||
| <script setup lang="ts"> | ||||
| import { type RouterLinkProps } from 'vue-router' | ||||
| import { TABS_INJECTION_KEY } from '~/injection-keys' | ||||
| import { type TabProps, TABS_INJECTION_KEY } from '~/injection-keys' | ||||
| import { whenever } from '@vueuse/core' | ||||
| import { inject, ref } from 'vue' | ||||
| 
 | ||||
| export type Props = { | ||||
|   title: string, | ||||
|   to?: RouterLinkProps['to'] | ||||
|   icon?: string | ||||
| } | ||||
| 
 | ||||
| const props = defineProps<Props>() | ||||
| const props = defineProps<TabProps>() | ||||
| 
 | ||||
| const { currentTitle, tabs } = inject(TABS_INJECTION_KEY, { | ||||
|   currentTitle: ref(props.title), | ||||
|  |  | |||
|  | @ -1,9 +1,7 @@ | |||
| <script setup lang="ts"> | ||||
| import { TABS_INJECTION_KEY } from '~/injection-keys' | ||||
| import { type TabProps, TABS_INJECTION_KEY } from '~/injection-keys' | ||||
| import { computed, provide, reactive, ref, watch } from 'vue' | ||||
| 
 | ||||
| import { type Props as TabProps } from '~/components/ui/Tab.vue' | ||||
| 
 | ||||
| import Button from '~/components/ui/Button.vue' | ||||
| import Link from '~/components/ui/Link.vue' | ||||
| import { useRoute } from 'vue-router' | ||||
|  |  | |||
|  | @ -39,16 +39,12 @@ const styles = { | |||
|   }[a!]) | ||||
| } as const | ||||
| 
 | ||||
| const getStyle = (props : Partial<AlignmentProps>) => ([key, value]: Entry<AlignmentProps>) => | ||||
| const getStyle = (props : Partial<AlignmentProps>) => ([key, value]: Entry<AlignmentProps>):string => | ||||
|   ( | ||||
|     typeof styles[key] === 'function' | ||||
|       ? (styles[key]( | ||||
|     typeof styles[key] === 'string' | ||||
|       ? styles[key] | ||||
|       // @ts-expect-error We know that props[key] is a value accepted by styles[key]. The ts compiler is not so smart.
 | ||||
|           (key in props && props[key]) | ||||
|             ? props[((props[key]), (key))] | ||||
|             : value | ||||
|         )) | ||||
|       : styles[key] | ||||
|       : (styles[key]((key in props && props[key]) ? props[((props[key]), (key))] : value)) | ||||
|   ) | ||||
| 
 | ||||
| const merge = (rules: string[]) => (attributes: HTMLAttributes = {}) => | ||||
|  |  | |||
|  | @ -111,6 +111,9 @@ export const useQueue = createGlobalState(() => { | |||
|       const { uploads } = await axios.get(`tracks/${track.id}/`) | ||||
|         .then(response => response.data as Track, () => ({ uploads: [] as Upload[] })) | ||||
| 
 | ||||
|       // TODO: Either make `track` a writable ref or implement the client/cache model
 | ||||
|       // See Issue: https://dev.funkwhale.audio/funkwhale/funkwhale/-/issues/2437
 | ||||
|       // @ts-expect-error `track` is read-only
 | ||||
|       track.uploads = uploads | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -2,7 +2,6 @@ import type { Track, Artist, Album, Playlist, Library, Channel, Actor } from '~/ | |||
| import type { components } from '~/generated/types' | ||||
| import type { ContentFilter } from '~/store/moderation' | ||||
| 
 | ||||
| import { useCurrentElement } from '@vueuse/core' | ||||
| import { computed, markRaw, ref } from 'vue' | ||||
| import { i18n } from '~/init/locale' | ||||
| import { useStore } from '~/store' | ||||
|  | @ -154,7 +153,7 @@ export default (props: PlayOptionsProps) => { | |||
|     return tracks.filter(track => track.uploads?.length).map(markRaw) | ||||
|   } | ||||
| 
 | ||||
|   const el = useCurrentElement() | ||||
|   // const el = useCurrentElement()
 | ||||
| 
 | ||||
|   const enqueue = async () => { | ||||
|     const tracks = await getPlayableTracks() | ||||
|  |  | |||
|  | @ -4,10 +4,12 @@ import { ref } from 'vue' | |||
| 
 | ||||
| export default () => { | ||||
|   const pageQuery = useRouteQuery<string>('page', '1') | ||||
|   const page = ref() | ||||
|   const page = ref<number>() | ||||
|   syncRef(pageQuery, page, { | ||||
|     transform: { | ||||
|       ltr: (left) => +left, | ||||
|       // TODO: Why toString?
 | ||||
|       // @ts-expect-error string vs. number
 | ||||
|       rtl: (right) => right.toString() | ||||
|     } | ||||
|   }) | ||||
|  |  | |||
|  | @ -42,13 +42,13 @@ const styles = { | |||
|   ...widths, ...sizes | ||||
| } as const satisfies Record<Key, string | ((w: string) => string)> | ||||
| 
 | ||||
| // The `lint:tsc` script more errors here than the language server is happy.
 | ||||
| // TODO: Fix this Issue: https://dev.funkwhale.audio/funkwhale/funkwhale/-/issues/2437
 | ||||
| const getStyle = (props: Partial<WidthProps>) => (key: Key):string => | ||||
|   key in props | ||||
|     ? typeof styles[key] === 'function' | ||||
|       ? styles[key]( | ||||
|       // @ts-expect-error Typescript is hard. Make the typescript compiler understand `key in props`
 | ||||
|         props[key] | ||||
|       ) | ||||
|       ? styles[key](props[key]) | ||||
|       : styles[key] as string | ||||
|     : '' | ||||
| 
 | ||||
|  |  | |||
|  | @ -70,6 +70,8 @@ export const install: InitModule = ({ store }) => { | |||
|       const { current } = store.state.radios | ||||
| 
 | ||||
|       if (current.clientOnly) { | ||||
|         // TODO: Type this event
 | ||||
|         // @ts-expect-error untyped event
 | ||||
|         await CLIENT_RADIOS[current.type].handleListen(current, event) | ||||
|       } | ||||
|     } | ||||
|  |  | |||
|  | @ -1,5 +1,11 @@ | |||
| import { type InjectionKey, type Ref } from 'vue' | ||||
| import { type Props as TabProps } from '~/components/ui/Tab.vue' | ||||
| import { type RouterLinkProps } from 'vue-router' | ||||
| 
 | ||||
| export type TabProps = { | ||||
|   title: string, | ||||
|   to?: RouterLinkProps['to'] | ||||
|   icon?: string | ||||
| } | ||||
| 
 | ||||
| export const TABS_INJECTION_KEY = Symbol('tabs') as InjectionKey<{ | ||||
|   tabs: TabProps[] | ||||
|  |  | |||
|  | @ -16,10 +16,6 @@ export interface State { | |||
|   settings: Settings | ||||
| } | ||||
| 
 | ||||
| type TotalCount = { | ||||
|   total: number | ||||
| } | ||||
| 
 | ||||
| // export interface NodeInfo {
 | ||||
| //   version: string;
 | ||||
| //   software: {
 | ||||
|  |  | |||
|  | @ -10,7 +10,8 @@ import Alert from '~/components/ui/Alert.vue' | |||
| import Button from '~/components/ui/Button.vue' | ||||
| import Modal from '~/components/ui/Modal.vue' | ||||
| import Input from '~/components/ui/Input.vue' | ||||
| import FileUploadWidget from '~/components/library/FileUploadWidget.vue' | ||||
| 
 | ||||
| // TODO: Delete this file once all upload functionality is moved to the new UI. | ||||
| 
 | ||||
| const { t } = useI18n() | ||||
| 
 | ||||
|  | @ -38,13 +39,15 @@ const combinedFileSize = computed(() => bytesToHumanSize( | |||
| )) | ||||
| 
 | ||||
| // Actions | ||||
| const processFiles = (fileList: FileList) => { | ||||
|   if (!uploads.currentUploadGroup) return | ||||
| 
 | ||||
|   for (const file of fileList) { | ||||
|     uploads.currentUploadGroup.queueUpload(file) | ||||
|   } | ||||
| } | ||||
| // TODO: Is this needed? | ||||
| // const processFiles = (fileList: FileList) => { | ||||
| //   if (!uploads.currentUploadGroup) return | ||||
| 
 | ||||
| //   for (const file of fileList) { | ||||
| //     uploads.currentUploadGroup.queueUpload(file) | ||||
| //   } | ||||
| // } | ||||
| 
 | ||||
| const router = useRouter() | ||||
| const cancel = () => { | ||||
|  | @ -53,14 +56,14 @@ const cancel = () => { | |||
|   uploads.currentUploadGroup = undefined | ||||
| 
 | ||||
|   if (uploads.queue.length > 0) { | ||||
|     return router.push('/upload/running') | ||||
|     router.push('/upload/running') | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| const continueInBackground = () => { | ||||
|   libraryOpen.value = false | ||||
|   uploads.currentUploadGroup = undefined | ||||
|   return router.push('/upload/running') | ||||
|   router.push('/upload/running') | ||||
| } | ||||
| 
 | ||||
| // TODO (whole file): Delete this file, please. | ||||
|  | @ -106,12 +109,16 @@ const isOpen = computed({ | |||
|       </Alert> | ||||
|     </template> | ||||
| 
 | ||||
|     <!-- TODO: Use a file input. We haven't implemented this yet. | ||||
|     We could say v-model can be of type `string | number | File | File[]` | ||||
|     and then implement this functionality. --> | ||||
|     <!-- v-model="processFiles" --> | ||||
|     <!-- @vue-ignore --> | ||||
|     <Input | ||||
|       type="file" | ||||
|       :accept="['.flac', '.ogg', '.opus', '.mp3', '.aac', '.aif', '.aiff', '.m4a'].join(', ')" | ||||
|       multiple | ||||
|       auto-reset | ||||
|       @files="processFiles" | ||||
|     /> | ||||
| 
 | ||||
|     <!-- Upload path --> | ||||
|  |  | |||
|  | @ -14,7 +14,6 @@ import Popover from '~/components/ui/Popover.vue' | |||
| import PopoverItem from '~/components/ui/popover/PopoverItem.vue' | ||||
| import PopoverSubmenu from '~/components/ui/popover/PopoverSubmenu.vue' | ||||
| import Spacer from '~/components/ui/Spacer.vue' | ||||
| import Pill from '~/components/ui/Pill.vue' | ||||
| 
 | ||||
| const route = useRoute() | ||||
| const store = useStore() | ||||
|  |  | |||
|  | @ -23,7 +23,9 @@ | |||
| //   return Metadata.parseBlob(file).then(metadata => metadata.common)
 | ||||
| // }
 | ||||
| 
 | ||||
| // @ts-expect-error This is not installed...?
 | ||||
| import * as jsmediaTags from 'jsmediatags/dist/jsmediatags.min.js' | ||||
| // @ts-expect-error This is not installed...?
 | ||||
| import type { ShortcutTags } from 'jsmediatags' | ||||
| 
 | ||||
| const REQUIRED_TAGS = ['title', 'artist', 'album'] | ||||
|  | @ -47,6 +49,7 @@ export const getCoverUrl = async (tags: Tags): Promise<string | undefined> => { | |||
| export const getTags = async (file: File) => { | ||||
|   return new Promise((resolve, reject) => { | ||||
|     jsmediaTags.read(file, { | ||||
|       // @ts-expect-error Please type `tags`
 | ||||
|       onSuccess: ({ tags }) => { | ||||
|         if (tags.picture?.data) { | ||||
|           tags.picture.data = new Uint8Array(tags.picture.data) | ||||
|  | @ -59,6 +62,7 @@ export const getTags = async (file: File) => { | |||
| 
 | ||||
|         resolve(tags) | ||||
|       }, | ||||
|       // @ts-expect-error Please type `error`
 | ||||
|       onError: (error) => reject(error) | ||||
|     }) | ||||
|   }) | ||||
|  |  | |||
|  | @ -4,8 +4,8 @@ import { computed, ref, type Ref } from 'vue' | |||
| import { useStore } from '~/store' | ||||
| import axios from 'axios' | ||||
| 
 | ||||
| type Item = { type: 'custom' | 'preset', label: string } | ||||
| type Model = { currents: Item[], others?: Item[] } | ||||
| export type Item = { type: 'custom' | 'preset', label: string } | ||||
| export type Model = { currents: Item[], others?: Item[] } | ||||
| 
 | ||||
| /** | ||||
|  * Load and cache all tags. | ||||
|  |  | |||
|  | @ -190,9 +190,13 @@ export const useUploadsStore = defineStore('uploads', () => { | |||
|       const upload = group.queue.find(entry => entry.guid === event.upload.uuid) | ||||
|       if (!upload) continue | ||||
| 
 | ||||
|       if (event.new_status !== 'failed') { | ||||
|       if (event.new_status !== 'errored') { | ||||
|         // TODO: Find out what other field to use here
 | ||||
|         // @ts-expect-error wrong field
 | ||||
|         upload.importedAt = event.upload.import_date | ||||
|       } else { | ||||
|         // TODO: Add second parameter `error`
 | ||||
|         // @ts-expect-error missing parameter
 | ||||
|         upload.fail('import-failed') | ||||
|       } | ||||
|       break | ||||
|  |  | |||
|  | @ -16,7 +16,6 @@ import useMarkdown from '~/composables/useMarkdown' | |||
| 
 | ||||
| import Layout from '~/components/ui/Layout.vue' | ||||
| import Loader from '~/components/ui/Loader.vue' | ||||
| import Spacer from '~/components/ui/Spacer.vue' | ||||
| import Header from '~/components/ui/Header.vue' | ||||
| import Toggle from '~/components/ui/Toggle.vue' | ||||
| import Button from '~/components/ui/Button.vue' | ||||
|  |  | |||
|  | @ -1,6 +1,5 @@ | |||
| <script setup lang="ts"> | ||||
| import { useI18n } from 'vue-i18n' | ||||
| import { computed } from 'vue' | ||||
| 
 | ||||
| import EditsCardList from '~/components/manage/library/EditsCardList.vue' | ||||
| 
 | ||||
|  | @ -15,9 +14,11 @@ withDefaults(defineProps<Props>(), { | |||
| }) | ||||
| 
 | ||||
| const { t } = useI18n() | ||||
| const labels = computed(() => ({ | ||||
|   title: t('views.admin.library.EditsList.title') | ||||
| })) | ||||
| 
 | ||||
| // TODO: Do we want to use this title? | ||||
| // const labels = computed(() => ({ | ||||
| //   title: t('views.admin.library.EditsList.title') | ||||
| // })) | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|  |  | |||
|  | @ -8,7 +8,6 @@ import { useI18n } from 'vue-i18n' | |||
| import { useStore } from '~/store' | ||||
| 
 | ||||
| import axios from 'axios' | ||||
| import $ from 'jquery' | ||||
| 
 | ||||
| import Layout from '~/components/ui/Layout.vue' | ||||
| import Loader from '~/components/ui/Loader.vue' | ||||
|  | @ -108,6 +107,7 @@ fetchData() | |||
| const el = useCurrentElement() | ||||
| watch(object, async () => { | ||||
|   await nextTick() | ||||
|   // @ts-expect-error JQuery owhere to be found... | ||||
|   $(el.value).find('select.dropdown').dropdown() | ||||
| }) | ||||
| 
 | ||||
|  |  | |||
|  | @ -47,12 +47,14 @@ const logger = useLogger() | |||
| 
 | ||||
| const search = ref() | ||||
| 
 | ||||
| const page = usePage() | ||||
| const result = ref<BackendResponse<Report>>() | ||||
| 
 | ||||
| const { onOrderingUpdate, orderingString, paginateBy, ordering, orderingDirection } = useOrdering(props) | ||||
| const { onSearch, query, addSearchToken, getTokenValue } = useSmartSearch(props) | ||||
| 
 | ||||
| const page = usePage() | ||||
| const pages = computed(() => result.value?.count ? Math.ceil(result.value.count / paginateBy.value) : 0) | ||||
| 
 | ||||
| const orderingOptions: [OrderingField, keyof typeof sharedLabels.filters][] = [ | ||||
|   ['creation_date', 'creation_date'], | ||||
|   ['applied_date', 'applied_date'] | ||||
|  | @ -214,8 +216,9 @@ const labels = computed(() => ({ | |||
|     </div> | ||||
|     <div class="ui center aligned basic segment"> | ||||
|       <Pagination | ||||
|         v-if="result && result.count > paginateBy" | ||||
|         v-model:current="page" | ||||
|         v-if="page && result && result.count > paginateBy" | ||||
|         v-model:page="page" | ||||
|         :pages="pages" | ||||
|         :paginate-by="paginateBy" | ||||
|       /> | ||||
|     </div> | ||||
|  |  | |||
|  | @ -110,10 +110,10 @@ const labels = computed(() => ({ | |||
|           <Input | ||||
|             id="requests-search" | ||||
|             ref="search" | ||||
|             v-model="query" | ||||
|             name="search" | ||||
|             search | ||||
|             :label="t('views.admin.moderation.RequestsList.label.search')" | ||||
|             :value="query" | ||||
|             :placeholder="labels.searchPlaceholder" | ||||
|           /> | ||||
|         </form> | ||||
|  | @ -194,8 +194,9 @@ const labels = computed(() => ({ | |||
|       @handled="fetchData" | ||||
|     /> | ||||
|     <Pagination | ||||
|       v-if="result.count > paginateBy" | ||||
|       v-model:current="page" | ||||
|       v-if="page && result.count > paginateBy" | ||||
|       v-model:page="page" | ||||
|       v-model:pages="result.count" | ||||
|       :paginate-by="paginateBy" | ||||
|     /> | ||||
|   </template> | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| <script setup lang="ts"> | ||||
| import { useI18n } from 'vue-i18n' | ||||
| import { computed, ref } from 'vue' | ||||
| import { useRoute } from 'vue-router' | ||||
| 
 | ||||
| import Layout from '~/components/ui/Layout.vue' | ||||
| import Nav from '~/components/ui/Nav.vue' | ||||
|  | @ -33,6 +34,6 @@ const tabs = ref([ | |||
|   > | ||||
|     <Nav v-model="tabs" /> | ||||
| 
 | ||||
|     <router-view :key="$route.fullPath" /> | ||||
|     <router-view :key="useRoute().fullPath" /> | ||||
|   </Layout> | ||||
| </template> | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| <script setup lang="ts"> | ||||
| import type { ImportStatus, PrivacyLevel, Upload, BackendResponse } from '~/types' | ||||
| import type { ImportStatus, PrivacyLevel, Upload } from '~/types' | ||||
| import type { SmartSearchProps } from '~/composables/navigation/useSmartSearch' | ||||
| import type { OrderingProps } from '~/composables/navigation/useOrdering' | ||||
| import type { RouteRecordName } from 'vue-router' | ||||
|  | @ -595,12 +595,12 @@ const getPrivacyLevelChoice = (privacyLevel: PrivacyLevel) => { | |||
|     </template> | ||||
|   </action-table> | ||||
|   <Pagination | ||||
|     v-if="result && result.count > paginateBy" | ||||
|     v-if="page && result && result.count > paginateBy" | ||||
|     v-model:page="page" | ||||
|     :pages="Math.ceil(result.count / paginateBy)" | ||||
|   /> | ||||
| 
 | ||||
|   <span v-if="result && result.results.length > 0"> | ||||
|   <span v-if="page && result && result.results.length > 0"> | ||||
|     {{ t('components.manage.library.UploadsTable.pagination.results', { start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count }) }} | ||||
|   </span> | ||||
| </template> | ||||
|  |  | |||
|  | @ -1,13 +1,12 @@ | |||
| <script setup lang="ts"> | ||||
| import type { BackendError } from '~/types' | ||||
| 
 | ||||
| import { computed, ref, onMounted, nextTick } from 'vue' | ||||
| import { computed, ref } from 'vue' | ||||
| import { useI18n } from 'vue-i18n' | ||||
| import { useRouter } from 'vue-router' | ||||
| 
 | ||||
| import Input from '~/components/ui/Input.vue' | ||||
| import Button from '~/components/ui/Button.vue' | ||||
| import Spacer from '~/components/ui/Spacer.vue' | ||||
| import Layout from '~/components/ui/Layout.vue' | ||||
| import Link from '~/components/ui/Link.vue' | ||||
| 
 | ||||
|  |  | |||
|  | @ -12,7 +12,6 @@ import UserFollowButton from '~/components/federation/UserFollowButton.vue' | |||
| import axios from 'axios' | ||||
| 
 | ||||
| import useErrorHandler from '~/composables/useErrorHandler' | ||||
| import useReport from '~/composables/moderation/useReport' | ||||
| import RenderedDescription from '~/components/common/RenderedDescription.vue' | ||||
| 
 | ||||
| import Layout from '~/components/ui/Layout.vue' | ||||
|  | @ -35,7 +34,6 @@ const props = withDefaults(defineProps<Props>(), { | |||
|   domain: null | ||||
| }) | ||||
| 
 | ||||
| const { report, getReportableObjects } = useReport() | ||||
| const store = useStore() | ||||
| 
 | ||||
| const object = ref<components['schemas']['FullActor'] | null>(null) | ||||
|  | @ -43,7 +41,9 @@ const object = ref<components['schemas']['FullActor'] | null>(null) | |||
| const actorColor = computed(() => intToRGB(hashCode(object.value?.full_username))) | ||||
| const defaultAvatarStyle = computed(() => ({ backgroundColor: `#${actorColor.value}` })) | ||||
| 
 | ||||
| const displayName = computed(() => object.value?.name ?? object.value?.preferred_username) | ||||
| // TODO: Check if still needed | ||||
| //const displayName = computed(() => object.value?.name ?? object.value?.preferred_username) | ||||
| 
 | ||||
| const fullUsername = computed(() => props.domain | ||||
|   ? `${props.username}@${props.domain}` | ||||
|   : `${props.username}@${store.getters['instance/domain']}` | ||||
|  | @ -79,7 +79,6 @@ const fetchData = async () => { | |||
| } | ||||
| 
 | ||||
| watch(props, fetchData, { immediate: true }) | ||||
| const recentActivity = ref(0) | ||||
| 
 | ||||
| const { copy, copied, isSupported } = useClipboard() | ||||
| 
 | ||||
|  | @ -106,13 +105,19 @@ const tabs = ref([{ | |||
|     main | ||||
|   > | ||||
|     <!-- TODO: Translate Edit Link --> | ||||
|     <!-- TODO: `yarn lint:tsc` doesn't understand the `Prop` type for `Header` while the language server does. It may be a question of typescript version... Investigate and fix! --> | ||||
|     <!-- @vue-ignore --> | ||||
|     <Header | ||||
|       :h1="props.username" | ||||
|       :action="{ | ||||
|         text:'Edit profile', | ||||
|         // @ts-ignore | ||||
|         to:'/settings', | ||||
|         // @ts-ignore | ||||
|         primary: true, | ||||
|         // @ts-ignore | ||||
|         solid: true, | ||||
|         // @ts-ignore | ||||
|         icon: 'bi-pencil-fill' | ||||
|       }" | ||||
|       style="margin-top: 58px;" | ||||
|  | @ -169,6 +174,8 @@ const tabs = ref([{ | |||
|         flex | ||||
|         no-gap | ||||
|       > | ||||
|         <!-- TODO: Fix error with `$event` not being the right type --> | ||||
|         <!-- @vue-ignore --> | ||||
|         <RenderedDescription | ||||
|           :content="{ html: object?.summary?.html || '' }" | ||||
|           :field-name="'summary'" | ||||
|  |  | |||
|  | @ -142,6 +142,9 @@ const remove = async () => { | |||
| 
 | ||||
| const updateSubscriptionCount = (delta: number) => { | ||||
|   if (object.value) { | ||||
|     // TODO: Store a modified copy in the cache or on the db instead of mutating the object in-memory. | ||||
|     // #2438 | ||||
|     // @ts-expect-error Property 'subscriptions_count' is readonly on type 'Channel' | ||||
|     object.value.subscriptions_count -= delta | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -139,12 +139,17 @@ const showCreateModal = ref(false) | |||
|     stack | ||||
|     main | ||||
|   > | ||||
|     <!-- TODO: `yarn lint:tsc` doesn't understand the `Prop` type for `Header` while the language server does. It may be a question of typescript version... Investigate and fix! --> | ||||
|     <!-- @vue-ignore --> | ||||
|     <Header | ||||
|       :h1="t('views.channels.SubscriptionsList.title')" | ||||
|       :action="{ | ||||
|         text: t('views.channels.SubscriptionsList.link.addNew'), | ||||
|         // @ts-ignore | ||||
|         onClick: () => { showSubscribeModal = true }, | ||||
|         // @ts-ignore | ||||
|         primary: true, | ||||
|         // @ts-ignore | ||||
|         icon: 'bi-plus' | ||||
|       }" | ||||
|       large-section-heading | ||||
|  | @ -194,13 +199,17 @@ const showCreateModal = ref(false) | |||
|       :show-modification-date="true" | ||||
|       :filters="{q: subscribedQuery, subscribed: 'true'}" | ||||
|     /> | ||||
|     <!-- TODO: Translations --> | ||||
|     <!-- TODO: `yarn lint:tsc` doesn't understand the `Prop` type for `Header` while the language server does. It may be a question of typescript version... Investigate and fix! --> | ||||
|     <!-- @vue-ignore --> | ||||
|     <Header | ||||
|       :h1="t('views.auth.ProfileOverview.header.channels')" | ||||
|       :action="{ | ||||
|         text: t('views.channels.SubscriptionsList.link.addNew'), | ||||
|         // @ts-ignore | ||||
|         onClick: () => { showCreateModal = true }, | ||||
|         // @ts-ignore | ||||
|         icon: 'bi-plus', | ||||
|         // @ts-ignore | ||||
|         primary: true | ||||
|       }" | ||||
|       large-section-heading | ||||
|  |  | |||
|  | @ -64,12 +64,17 @@ const showSubscribeModal = ref(false) | |||
|     stack | ||||
|     main | ||||
|   > | ||||
|     <!-- TODO: `yarn lint:tsc` doesn't understand the `Prop` type for `Header` while the language server does. It may be a question of typescript version... Investigate and fix! --> | ||||
|     <!-- @vue-ignore --> | ||||
|     <Header | ||||
|       :h1="labels.title" | ||||
|       :action="{ | ||||
|         text: t('views.channels.SubscriptionsList.link.addNew'), | ||||
|         // @ts-ignore | ||||
|         onClick: () => { showSubscribeModal = true }, | ||||
|         // @ts-ignore | ||||
|         primary: true, | ||||
|         // @ts-ignore | ||||
|         icon: 'bi-plus' | ||||
|       }" | ||||
|       page-heading | ||||
|  |  | |||
|  | @ -6,8 +6,6 @@ import { useI18n } from 'vue-i18n' | |||
| import { useStore } from '~/store' | ||||
| import { computed } from 'vue' | ||||
| 
 | ||||
| import moment from 'moment' | ||||
| 
 | ||||
| import TagsList from '~/components/tags/List.vue' | ||||
| 
 | ||||
| interface Props { | ||||
|  | @ -25,20 +23,20 @@ const imageUrl = computed(() => props.channel.artist?.cover | |||
|   ? store.getters['instance/absoluteUrl'](props.channel.artist?.cover.urls.medium_square_crop) | ||||
|   : fallbackImageUrl | ||||
| ) | ||||
| const urlId = computed(() => props.channel.actor?.is_local | ||||
|   ? props.channel.actor.preferred_username | ||||
|   : props.channel.actor | ||||
|     ? props.channel.actor.full_username | ||||
|     : props.channel.uuid | ||||
| ) | ||||
| 
 | ||||
| // TODO: Find out if still useful: | ||||
| // const urlId = computed(() => props.channel.actor?.is_local | ||||
| //   ? props.channel.actor.preferred_username | ||||
| //   : props.channel.actor | ||||
| //     ? props.channel.actor.full_username | ||||
| //     : props.channel.uuid | ||||
| // ) | ||||
| 
 | ||||
| const { t } = useI18n() | ||||
| const updatedTitle = computed(() => { | ||||
|   const date = momentFormat(new Date(props.channel.artist?.modification_date ?? '1970-01-01')) | ||||
|   return t('components.audio.ChannelCard.title', { date }) | ||||
| }) | ||||
| 
 | ||||
| const updatedAgo = computed(() => moment(props.channel.artist?.modification_date).fromNow()) | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|  |  | |||
|  | @ -55,18 +55,30 @@ const privacyTooltips = (level: PrivacyLevel) => `Visibility: ${sharedLabels.fie | |||
|           <human-date :date="library.creation_date" /> | ||||
|         </span> | ||||
|       </div> | ||||
| 
 | ||||
|       <!-- TODO: Add `description` field to `Library` --> | ||||
|       <!-- @vue-ignore --> | ||||
|       <div class="description"> | ||||
|         {{ library.description }} | ||||
|         {{ | ||||
|           // @ts-expect-error Property 'description' does not exist on type 'Library' | ||||
|           library.description | ||||
|         }} | ||||
|         <div class="ui hidden divider" /> | ||||
|       </div> | ||||
| 
 | ||||
|       <div class="content"> | ||||
|         <!-- TODO: Add `size` field to `Library` (or find out how else to load size) --> | ||||
|         <!-- @vue-ignore --> | ||||
|         <span | ||||
|           v-if="library.size" | ||||
|           class="right floated" | ||||
|           :data-tooltip="sizeLabel" | ||||
|         > | ||||
|           <i class="database icon" /> | ||||
|           {{ humanSize(library.size) }} | ||||
|           {{ | ||||
|             // @ts-expect-error Property 'size' does not exist on type 'Library' | ||||
|             humanSize(library.size) | ||||
|           }} | ||||
|         </span> | ||||
|         <i class="music icon" /> | ||||
|         {{ t('views.content.libraries.Card.meta.tracks', library.uploads_count) }} | ||||
|  |  | |||
|  | @ -56,8 +56,6 @@ const props = withDefaults(defineProps<Props>(), { | |||
|   orderingConfigName: undefined | ||||
| }) | ||||
| 
 | ||||
| const search = ref() | ||||
| 
 | ||||
| const page = usePage() | ||||
| const result = ref<BackendResponse<Upload>>() | ||||
| 
 | ||||
|  | @ -347,7 +345,7 @@ const getImportStatusChoice = (importStatus: ImportStatus) => { | |||
|   </action-table> | ||||
|   <div> | ||||
|     <Pagination | ||||
|       v-if="result && result.count > paginateBy" | ||||
|       v-if="page && result && result.count > paginateBy" | ||||
|       v-model:page="page" | ||||
|       :pages="Math.ceil(result.count / paginateBy)" | ||||
|     /> | ||||
|  |  | |||
|  | @ -37,6 +37,9 @@ const labels = computed(() => ({ | |||
| })) | ||||
| 
 | ||||
| const currentVisibilityLevel = ref(props.library?.privacy_level ?? 'me') | ||||
| 
 | ||||
| // TODO: Add 'description' to the Library type | ||||
| // @ts-expect-error Property 'description' does not exist on type 'Library' | ||||
| const currentDescription = ref(props.library?.description ?? '') | ||||
| const currentName = ref(props.library?.name ?? '') | ||||
| 
 | ||||
|  |  | |||
|  | @ -48,7 +48,9 @@ const isLoadingFollow = ref(false) | |||
| const showScan = ref(false) | ||||
| const latestScan = ref(props.initialLibrary.latest_scan) | ||||
| 
 | ||||
| const scanProgress = computed(() => Math.min(latestScan.value.processed_files * 100 / latestScan.value.total_files, 100)) | ||||
| const scanProgress = computed(() => latestScan.value && latestScan.value.processed_files && latestScan.value.total_files | ||||
|   ? Math.min(latestScan.value.processed_files * 100 / latestScan.value.total_files, 100) | ||||
|   : 0) | ||||
| const scanStatus = computed(() => latestScan.value?.status ?? 'unknown') | ||||
| const canLaunchScan = computed(() => scanStatus.value !== 'pending' && scanStatus.value !== 'scanning') | ||||
| const radioPlayable = computed(() => ( | ||||
|  | @ -191,8 +193,13 @@ const isOpen = ref(false) | |||
|     </template> | ||||
| 
 | ||||
|     <div class="content"> | ||||
|       <!-- TODO: Add `description` field to `Library` --> | ||||
|       <!-- @vue-ignore --> | ||||
|       <div class="description"> | ||||
|         {{ library.description }} | ||||
|         {{ | ||||
|           // @ts-ignore | ||||
|           library.description | ||||
|         }} | ||||
|       </div> | ||||
|       <Spacer :size="8" /> | ||||
|       <div | ||||
|  | @ -215,7 +222,7 @@ const isOpen = ref(false) | |||
|           <i class="bi bi-check-circle" /> | ||||
|           {{ t('views.content.remote.Card.label.scanSuccess') }} | ||||
|         </template> | ||||
|         <template v-else-if="latestScan.status === 'finished' && latestScan.errored_files > 0"> | ||||
|         <template v-else-if="latestScan.status === 'finished' && latestScan.errored_files && latestScan.errored_files > 0"> | ||||
|           <i class="bi bi-exclamation-circle" /> | ||||
|           {{ t('views.content.remote.Card.label.scanPartialSuccess') }} | ||||
|         </template> | ||||
|  |  | |||
|  | @ -37,8 +37,9 @@ fetchData() | |||
| 
 | ||||
| const getLibraryFromFollow = (follow: LibraryFollow) => { | ||||
|   const { target } = follow | ||||
|   target.follow = follow | ||||
|   return target as Library | ||||
|   // TODO: Actually load the target from the database or the cache. Use `client` with cache. | ||||
|   // @ts-expect-error target is a string, not a library! | ||||
|   return ({ ...target, follow: follow } as Library) | ||||
| } | ||||
| 
 | ||||
| const scanResult = ref() | ||||
|  |  | |||
|  | @ -18,6 +18,7 @@ defineProps<Props>() | |||
| <template> | ||||
|   <section> | ||||
|     <album-widget | ||||
|       v-if="object.uuid" | ||||
|       :key="String(object.uploads_count)" | ||||
|       :header="false" | ||||
|       :search="true" | ||||
|  |  | |||
|  | @ -4,10 +4,8 @@ import type { Library } from '~/types' | |||
| import ArtistWidget from '~/components/artist/Widget.vue' | ||||
| 
 | ||||
| import { useI18n } from 'vue-i18n' | ||||
| import { useStore } from '~/store' | ||||
| 
 | ||||
| const { t } = useI18n() | ||||
| const store = useStore() | ||||
| 
 | ||||
| interface Props { | ||||
|   object: Library | ||||
|  | @ -20,6 +18,7 @@ defineProps<Props>() | |||
| <template> | ||||
|   <section> | ||||
|     <artist-widget | ||||
|       v-if="object.uuid" | ||||
|       :key="object.uploads_count" | ||||
|       ref="artists" | ||||
|       :header="false" | ||||
|  |  | |||
|  | @ -2,6 +2,9 @@ | |||
| import type { Library } from '~/types' | ||||
| 
 | ||||
| import TrackTable from '~/components/audio/track/Table.vue' | ||||
| import { useI18n } from 'vue-i18n' | ||||
| 
 | ||||
| const { t } = useI18n() | ||||
| 
 | ||||
| interface Props { | ||||
|   object: Library | ||||
|  |  | |||
|  | @ -51,6 +51,10 @@ fetchData() | |||
| const updateApproved = async (follow: LibraryFollow, approved: boolean) => { | ||||
|   try { | ||||
|     await axios.post(`federation/follows/library/${follow.uuid}/${approved ? 'accept' : 'reject'}/`) | ||||
| 
 | ||||
|     // TODO: This is not how Axios works. You have to send a request with | ||||
|     // the correct type as a parameter. | ||||
|     // @ts-expect-error Post this with the axios payload: { ...follow, approved } | ||||
|     follow.approved = approved | ||||
|   } catch (error) { | ||||
|     useErrorHandler(error as Error) | ||||
|  |  | |||
|  | @ -194,9 +194,9 @@ const tabs = ref([{ | |||
|         <i class="bi bi-music-note-list" /> | ||||
|         {{ t('views.library.LibraryBase.meta.tracks', object.uploads_count) }} | ||||
|       </span> | ||||
|       <span v-if="object && 'size' in object && object.size"> | ||||
|       <span v-if="object && 'size' in object && object.size && typeof object.size === 'number'"> | ||||
|         <i class="bi bi-database-fill" /> | ||||
|         {{ humanSize(object.size) }} | ||||
|         {{ humanSize(object.size as number) }} | ||||
|       </span> | ||||
|     </Layout> | ||||
| 
 | ||||
|  | @ -213,15 +213,18 @@ const tabs = ref([{ | |||
|         v-if="!isOwner" | ||||
|       > | ||||
|         <library-follow-button | ||||
|           v-if="store.state.auth.authenticated" | ||||
|           v-if="store.state.auth.authenticated && object" | ||||
|           :library="object" | ||||
|         /> | ||||
|       </div> | ||||
|     </Layout> | ||||
| 
 | ||||
|     <!-- TODO: Add `description` field to library --> | ||||
|     <!-- @vue-ignore --> | ||||
|     <rendered-description | ||||
|       :content="object?.description ? {html: object?.description} : null" | ||||
|       :update-url="`channels/${object?.uuid}/`" | ||||
|       v-if="object && 'description' in object" | ||||
|       :content="{ html: object.description }" | ||||
|       :update-url="`channels/${object.uuid}/`" | ||||
|       :can-update="false" | ||||
|     /> | ||||
|     <Layout form> | ||||
|  |  | |||
|  | @ -117,16 +117,20 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value] | |||
|     stack | ||||
|     main | ||||
|   > | ||||
|     <!-- TODO: `yarn lint:tsc` doesn't understand the `Prop` type for `Header` while the language server does. It may be a question of typescript version... Investigate and fix! --> | ||||
|     <!-- @vue-ignore --> | ||||
|     <Header | ||||
|       v-if="store.state.auth.authenticated" | ||||
|       :h1="t('views.playlists.List.header.browse')" | ||||
|       page-heading | ||||
|       :action="{ | ||||
|         onClick: () => { store.commit('playlists/showModal', true) }, | ||||
|         text: t('views.playlists.List.button.manage'), | ||||
|         text: t('views.playlists.List.button.create'), | ||||
|         // @ts-ignore | ||||
|         icon: 'bi-plus', | ||||
|         // @ts-ignore | ||||
|         primary: true, | ||||
|         icon: 'bi-music-note-list', | ||||
|         ariaPressed: store.state.playlists.showModal || undefined | ||||
|         // @ts-ignore | ||||
|         onClick: () => { store.commit('playlists/showModal', true) } | ||||
|       }" | ||||
|     /> | ||||
|     <Header | ||||
|  | @ -243,7 +247,7 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value] | |||
|         </Button> | ||||
|       </Alert> | ||||
|       <Pagination | ||||
|         v-if="result && result.count > paginateBy" | ||||
|         v-if="page && result && result.count > paginateBy" | ||||
|         v-model:page="page" | ||||
|         style="grid-column: 1 / -1;" | ||||
|         :pages="Math.ceil(result.count/paginateBy)" | ||||
|  | @ -255,10 +259,10 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value] | |||
|       /> | ||||
|       <Spacer grow /> | ||||
|       <Pagination | ||||
|         v-if="result && result.count > paginateBy" | ||||
|         v-if="page && result && result.count > paginateBy" | ||||
|         v-model:page="page" | ||||
|         style="grid-column: 1 / -1;" | ||||
|         :pages="Math.ceil(result.count/paginateBy)" | ||||
|         style="grid-column: 1 / -1;" | ||||
|       /> | ||||
|     </Section> | ||||
|   </Layout> | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 upsiflu
						upsiflu