From 44df034e2c7cb7104dfb91120f13f45c840f6e59 Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Thu, 19 Mar 2026 16:35:41 +0000 Subject: [PATCH 01/49] fix: files.vue bugs before styling changes --- .../servers/files/editor/FileEditor.vue | 2 +- .../layouts/wrapped/hosting/manage/files.vue | 37 ++++--------------- 2 files changed, 9 insertions(+), 30 deletions(-) diff --git a/packages/ui/src/components/servers/files/editor/FileEditor.vue b/packages/ui/src/components/servers/files/editor/FileEditor.vue index 340879d14c..a4f707a070 100644 --- a/packages/ui/src/components/servers/files/editor/FileEditor.vue +++ b/packages/ui/src/components/servers/files/editor/FileEditor.vue @@ -4,7 +4,7 @@
['files', serverId, currentPath.value]), - queryFn: async ({ pageParam = 1 }) => { + queryFn: async () => { if (modulesLoaded) await modulesLoaded - return client.kyros.files_v0.listDirectory(currentPath.value, pageParam, 100) - }, - getNextPageParam: (lastPage, allPages) => { - const pageSize = 100 - if (lastPage.items.length >= pageSize) { - return allPages.length + 1 - } - - if (lastPage.current < lastPage.total) { - return lastPage.current + 1 - } - return undefined + return client.kyros.files_v0.listDirectory(currentPath.value, 1, 2000) }, staleTime: 30_000, - initialPageParam: 1, }) -const items = computed(() => directoryData.value?.pages.flatMap((page) => page.items) ?? []) +const items = computed(() => directoryData.value?.items ?? []) function prefetchDirectory(path: string) { - queryClient.prefetchInfiniteQuery({ + queryClient.prefetchQuery({ queryKey: ['files', serverId, path], queryFn: async () => { if (modulesLoaded) await modulesLoaded try { - return await client.kyros.files_v0.listDirectory(path, 1, 100) + return await client.kyros.files_v0.listDirectory(path, 1, 2000) } catch { return { items: [], total: 0, current: 1 } } }, - initialPageParam: 1, staleTime: 30_000, }) } @@ -1067,10 +1050,6 @@ const filteredItems = computed(() => { return applySort(result) }) -async function handleLoadMore() { - if (isFetchingNextPage.value || !hasNextPage.value) return - await fetchNextPage() -} function onKeydown(e: KeyboardEvent) { if ((e.ctrlKey || e.metaKey) && !e.shiftKey && e.key === 'z') { From bf5a565829f62a43506d90fb5718bd6a4a946242 Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Thu, 19 Mar 2026 17:33:07 +0000 Subject: [PATCH 02/49] feat: move files tab to shared layout structure --- .../TeleportOverflowMenu.vue | 0 .../components/servers/files/editor/index.ts | 4 +- .../servers/files/explorer/index.ts | 6 +- .../ui/src/components/servers/files/index.ts | 5 +- .../components/servers/files/modals/index.ts | 12 +- .../components/servers/files/upload/index.ts | 4 +- .../components/ContentCardItem.vue | 2 +- .../components/ContentModpackCard.vue | 2 +- .../components}/FileManagerError.vue | 3 +- .../files-tab/components}/FileNavbar.vue | 7 +- .../files-tab/components/FileTableHeader.vue} | 21 +- .../files-tab/components/FileTableRow.vue} | 52 +- .../files-tab/components}/FileVirtualList.vue | 29 +- .../components}/editor/FileEditor.vue | 71 +- .../components}/editor/FileImageViewer.vue | 3 +- .../modals/FileCreateItemModal.vue | 5 +- .../modals/FileDeleteItemModal.vue | 4 +- .../components}/modals/FileMoveItemModal.vue | 5 +- .../modals/FileRenameItemModal.vue | 5 +- .../modals/FileUploadConflictModal.vue | 3 +- .../modals/FileUploadZipUrlModal.vue | 18 +- .../upload/FileUploadDragAndDrop.vue | 0 .../components}/upload/FileUploadDropdown.vue | 5 +- .../files-tab/composables/file-search.ts | 20 + .../files-tab/composables/file-selection.ts | 52 + .../files-tab/composables/file-sorting.ts | 85 ++ .../files-tab/composables/file-undo-redo.ts | 102 ++ .../shared/files-tab/composables/index.ts | 4 + .../ui/src/layouts/shared/files-tab/index.ts | 3 + .../src/layouts/shared/files-tab/layout.vue | 710 +++++++++ .../files-tab/providers/file-manager.ts | 71 + .../shared/files-tab/providers/index.ts | 2 + .../ui/src/layouts/shared/files-tab/types.ts | 62 + .../layouts/wrapped/hosting/manage/files.vue | 1265 +++-------------- 34 files changed, 1434 insertions(+), 1208 deletions(-) rename packages/ui/src/components/{servers/files/explorer => base}/TeleportOverflowMenu.vue (100%) rename packages/ui/src/{components/servers/files/explorer => layouts/shared/files-tab/components}/FileManagerError.vue (93%) rename packages/ui/src/{components/servers/files => layouts/shared/files-tab/components}/FileNavbar.vue (95%) rename packages/ui/src/{components/servers/files/explorer/FileLabelBar.vue => layouts/shared/files-tab/components/FileTableHeader.vue} (74%) rename packages/ui/src/{components/servers/files/explorer/FileItem.vue => layouts/shared/files-tab/components/FileTableRow.vue} (87%) rename packages/ui/src/{components/servers/files/explorer => layouts/shared/files-tab/components}/FileVirtualList.vue (74%) rename packages/ui/src/{components/servers/files => layouts/shared/files-tab/components}/editor/FileEditor.vue (74%) rename packages/ui/src/{components/servers/files => layouts/shared/files-tab/components}/editor/FileImageViewer.vue (98%) rename packages/ui/src/{components/servers/files => layouts/shared/files-tab/components}/modals/FileCreateItemModal.vue (92%) rename packages/ui/src/{components/servers/files => layouts/shared/files-tab/components}/modals/FileDeleteItemModal.vue (94%) rename packages/ui/src/{components/servers/files => layouts/shared/files-tab/components}/modals/FileMoveItemModal.vue (90%) rename packages/ui/src/{components/servers/files => layouts/shared/files-tab/components}/modals/FileRenameItemModal.vue (92%) rename packages/ui/src/{components/servers/files => layouts/shared/files-tab/components}/modals/FileUploadConflictModal.vue (95%) rename packages/ui/src/{components/servers/files => layouts/shared/files-tab/components}/modals/FileUploadZipUrlModal.vue (91%) rename packages/ui/src/{components/servers/files => layouts/shared/files-tab/components}/upload/FileUploadDragAndDrop.vue (100%) rename packages/ui/src/{components/servers/files => layouts/shared/files-tab/components}/upload/FileUploadDropdown.vue (97%) create mode 100644 packages/ui/src/layouts/shared/files-tab/composables/file-search.ts create mode 100644 packages/ui/src/layouts/shared/files-tab/composables/file-selection.ts create mode 100644 packages/ui/src/layouts/shared/files-tab/composables/file-sorting.ts create mode 100644 packages/ui/src/layouts/shared/files-tab/composables/file-undo-redo.ts create mode 100644 packages/ui/src/layouts/shared/files-tab/composables/index.ts create mode 100644 packages/ui/src/layouts/shared/files-tab/index.ts create mode 100644 packages/ui/src/layouts/shared/files-tab/layout.vue create mode 100644 packages/ui/src/layouts/shared/files-tab/providers/file-manager.ts create mode 100644 packages/ui/src/layouts/shared/files-tab/providers/index.ts create mode 100644 packages/ui/src/layouts/shared/files-tab/types.ts diff --git a/packages/ui/src/components/servers/files/explorer/TeleportOverflowMenu.vue b/packages/ui/src/components/base/TeleportOverflowMenu.vue similarity index 100% rename from packages/ui/src/components/servers/files/explorer/TeleportOverflowMenu.vue rename to packages/ui/src/components/base/TeleportOverflowMenu.vue diff --git a/packages/ui/src/components/servers/files/editor/index.ts b/packages/ui/src/components/servers/files/editor/index.ts index 3f5fed318a..37a24bc724 100644 --- a/packages/ui/src/components/servers/files/editor/index.ts +++ b/packages/ui/src/components/servers/files/editor/index.ts @@ -1,2 +1,2 @@ -export { default as FileEditor } from './FileEditor.vue' -export { default as FileImageViewer } from './FileImageViewer.vue' +export { default as FileEditor } from '#ui/layouts/shared/files-tab/components/editor/FileEditor.vue' +export { default as FileImageViewer } from '#ui/layouts/shared/files-tab/components/editor/FileImageViewer.vue' diff --git a/packages/ui/src/components/servers/files/explorer/index.ts b/packages/ui/src/components/servers/files/explorer/index.ts index a19d7a7774..d4114b0f1c 100644 --- a/packages/ui/src/components/servers/files/explorer/index.ts +++ b/packages/ui/src/components/servers/files/explorer/index.ts @@ -1,5 +1 @@ -export { default as FileItem } from './FileItem.vue' -export { default as FileLabelBar } from './FileLabelBar.vue' -export { default as FileManagerError } from './FileManagerError.vue' -export { default as FileVirtualList } from './FileVirtualList.vue' -export { default as TeleportOverflowMenu } from './TeleportOverflowMenu.vue' +export { default as TeleportOverflowMenu } from '#ui/components/base/TeleportOverflowMenu.vue' diff --git a/packages/ui/src/components/servers/files/index.ts b/packages/ui/src/components/servers/files/index.ts index 93f3acbc51..4633d98b10 100644 --- a/packages/ui/src/components/servers/files/index.ts +++ b/packages/ui/src/components/servers/files/index.ts @@ -1,5 +1,8 @@ export * from './editor' export * from './explorer' -export { default as FileNavbar } from './FileNavbar.vue' export * from './modals' export * from './upload' +export { default as FileManagerError } from '#ui/layouts/shared/files-tab/components/FileManagerError.vue' +export { default as FileNavbar } from '#ui/layouts/shared/files-tab/components/FileNavbar.vue' +export { default as FileLabelBar } from '#ui/layouts/shared/files-tab/components/FileTableHeader.vue' +export { default as FileVirtualList } from '#ui/layouts/shared/files-tab/components/FileVirtualList.vue' diff --git a/packages/ui/src/components/servers/files/modals/index.ts b/packages/ui/src/components/servers/files/modals/index.ts index 1f2fd5694d..1fb0eaa290 100644 --- a/packages/ui/src/components/servers/files/modals/index.ts +++ b/packages/ui/src/components/servers/files/modals/index.ts @@ -1,6 +1,6 @@ -export { default as FileCreateItemModal } from './FileCreateItemModal.vue' -export { default as FileDeleteItemModal } from './FileDeleteItemModal.vue' -export { default as FileMoveItemModal } from './FileMoveItemModal.vue' -export { default as FileRenameItemModal } from './FileRenameItemModal.vue' -export { default as FileUploadConflictModal } from './FileUploadConflictModal.vue' -export { default as FileUploadZipUrlModal } from './FileUploadZipUrlModal.vue' +export { default as FileCreateItemModal } from '#ui/layouts/shared/files-tab/components/modals/FileCreateItemModal.vue' +export { default as FileDeleteItemModal } from '#ui/layouts/shared/files-tab/components/modals/FileDeleteItemModal.vue' +export { default as FileMoveItemModal } from '#ui/layouts/shared/files-tab/components/modals/FileMoveItemModal.vue' +export { default as FileRenameItemModal } from '#ui/layouts/shared/files-tab/components/modals/FileRenameItemModal.vue' +export { default as FileUploadConflictModal } from '#ui/layouts/shared/files-tab/components/modals/FileUploadConflictModal.vue' +export { default as FileUploadZipUrlModal } from '#ui/layouts/shared/files-tab/components/modals/FileUploadZipUrlModal.vue' diff --git a/packages/ui/src/components/servers/files/upload/index.ts b/packages/ui/src/components/servers/files/upload/index.ts index d44ae326ac..994e254d37 100644 --- a/packages/ui/src/components/servers/files/upload/index.ts +++ b/packages/ui/src/components/servers/files/upload/index.ts @@ -1,2 +1,2 @@ -export { default as FileUploadDragAndDrop } from './FileUploadDragAndDrop.vue' -export { default as FileUploadDropdown } from './FileUploadDropdown.vue' +export { default as FileUploadDragAndDrop } from '#ui/layouts/shared/files-tab/components/upload/FileUploadDragAndDrop.vue' +export { default as FileUploadDropdown } from '#ui/layouts/shared/files-tab/components/upload/FileUploadDropdown.vue' diff --git a/packages/ui/src/layouts/shared/content-tab/components/ContentCardItem.vue b/packages/ui/src/layouts/shared/content-tab/components/ContentCardItem.vue index d3e0ed5f56..9167808ce1 100644 --- a/packages/ui/src/layouts/shared/content-tab/components/ContentCardItem.vue +++ b/packages/ui/src/layouts/shared/content-tab/components/ContentCardItem.vue @@ -18,8 +18,8 @@ import BulletDivider from '#ui/components/base/BulletDivider.vue' import ButtonStyled from '#ui/components/base/ButtonStyled.vue' import Checkbox from '#ui/components/base/Checkbox.vue' import type { Option as OverflowMenuOption } from '#ui/components/base/OverflowMenu.vue' +import TeleportOverflowMenu from '#ui/components/base/TeleportOverflowMenu.vue' import Toggle from '#ui/components/base/Toggle.vue' -import TeleportOverflowMenu from '#ui/components/servers/files/explorer/TeleportOverflowMenu.vue' import { useVIntl } from '#ui/composables/i18n' import { commonMessages } from '#ui/utils/common-messages' import { truncatedTooltip } from '#ui/utils/truncate' diff --git a/packages/ui/src/layouts/shared/content-tab/components/ContentModpackCard.vue b/packages/ui/src/layouts/shared/content-tab/components/ContentModpackCard.vue index c9cc8d1020..e24f123599 100644 --- a/packages/ui/src/layouts/shared/content-tab/components/ContentModpackCard.vue +++ b/packages/ui/src/layouts/shared/content-tab/components/ContentModpackCard.vue @@ -21,7 +21,7 @@ import OverflowMenu, { type Option as OverflowMenuOption, } from '#ui/components/base/OverflowMenu.vue' import TagItem from '#ui/components/base/TagItem.vue' -import TeleportOverflowMenu from '#ui/components/servers/files/explorer/TeleportOverflowMenu.vue' +import TeleportOverflowMenu from '#ui/components/base/TeleportOverflowMenu.vue' import { useRelativeTime } from '#ui/composables/how-ago' import { defineMessages, useVIntl } from '#ui/composables/i18n' import { commonMessages } from '#ui/utils/common-messages' diff --git a/packages/ui/src/components/servers/files/explorer/FileManagerError.vue b/packages/ui/src/layouts/shared/files-tab/components/FileManagerError.vue similarity index 93% rename from packages/ui/src/components/servers/files/explorer/FileManagerError.vue rename to packages/ui/src/layouts/shared/files-tab/components/FileManagerError.vue index d609c92358..c7787bf7f0 100644 --- a/packages/ui/src/components/servers/files/explorer/FileManagerError.vue +++ b/packages/ui/src/layouts/shared/files-tab/components/FileManagerError.vue @@ -26,7 +26,8 @@ - - diff --git a/packages/ui/src/components/servers/files/explorer/FileVirtualList.vue b/packages/ui/src/layouts/shared/files-tab/components/FileVirtualList.vue similarity index 74% rename from packages/ui/src/components/servers/files/explorer/FileVirtualList.vue rename to packages/ui/src/layouts/shared/files-tab/components/FileVirtualList.vue index 210cb181cc..3807ceebf3 100644 --- a/packages/ui/src/components/servers/files/explorer/FileVirtualList.vue +++ b/packages/ui/src/layouts/shared/files-tab/components/FileVirtualList.vue @@ -38,6 +38,7 @@ @move="$emit('move', item)" @move-direct-to="$emit('moveDirectTo', $event)" @edit="$emit('edit', item)" + @navigate="$emit('navigate', item)" @hover="$emit('hover', item)" @contextmenu="(x, y) => $emit('contextmenu', item, x, y)" @toggle-select="$emit('toggle-select', item.path)" @@ -48,29 +49,31 @@ + + diff --git a/packages/ui/src/layouts/shared/files-tab/providers/file-manager.ts b/packages/ui/src/layouts/shared/files-tab/providers/file-manager.ts new file mode 100644 index 0000000000..20f284685b --- /dev/null +++ b/packages/ui/src/layouts/shared/files-tab/providers/file-manager.ts @@ -0,0 +1,71 @@ +import type { Component, ComputedRef, Ref } from 'vue' + +import { createContext } from '#ui/providers/create-context' + +import type { EditingFile, ExtractDryRunResult, FileItem, FileOperation } from '../types' + +export interface FileManagerContext { + // === Data === + items: Ref + loading: Ref + error: Ref + + // === Path & Navigation === + currentPath: Ref + navigateTo: (path: string) => void + + // === Editing === + editingFile: Ref + startEditing: (file: EditingFile) => void + stopEditing: () => void + + // === CRUD === + createItem: (name: string, type: 'file' | 'directory') => Promise + renameItem: (path: string, newName: string) => Promise + moveItem: (source: string, destination: string) => Promise + deleteItem: (path: string, recursive: boolean) => Promise + + // === File I/O === + readFile: (path: string) => Promise + readFileAsBlob: (path: string) => Promise + writeFile: (path: string, content: string) => Promise + downloadFile: (path: string, fileName: string) => Promise + + // === Upload === + uploadFile: (file: File) => void + + // === Refresh === + refresh: () => void + + // === Guards (optional) === + isBusy?: Ref | ComputedRef + busyTooltip?: Ref | ComputedRef + busyWarning?: Ref | ComputedRef + + // === Extraction (optional — hosting only) === + extractFile?: ( + path: string, + override: boolean, + dry: boolean, + ) => Promise + activeOperations?: Ref | ComputedRef + dismissOperation?: (id: string, action: 'dismiss' | 'cancel') => void + + // === Prefetch (optional) === + prefetchDirectory?: (path: string) => void + prefetchFile?: (path: string) => void + + // === Editor (provider supplies the lazy-loaded component) === + editorComponent: Ref + + // === Optional capabilities === + canRestart?: boolean + restartServer?: () => Promise + canShareToMclogs?: boolean + shareToMclogs?: (content: string) => Promise +} + +export const [injectFileManager, provideFileManager] = createContext( + 'FilePageLayout', + 'fileManagerContext', +) diff --git a/packages/ui/src/layouts/shared/files-tab/providers/index.ts b/packages/ui/src/layouts/shared/files-tab/providers/index.ts new file mode 100644 index 0000000000..8772a59621 --- /dev/null +++ b/packages/ui/src/layouts/shared/files-tab/providers/index.ts @@ -0,0 +1,2 @@ +export type { FileManagerContext } from './file-manager' +export { injectFileManager, provideFileManager } from './file-manager' diff --git a/packages/ui/src/layouts/shared/files-tab/types.ts b/packages/ui/src/layouts/shared/files-tab/types.ts new file mode 100644 index 0000000000..40848934b6 --- /dev/null +++ b/packages/ui/src/layouts/shared/files-tab/types.ts @@ -0,0 +1,62 @@ +export interface FileItem { + name: string + type: 'file' | 'directory' | 'symlink' + path: string + modified: number + created: number + size?: number + count?: number + target?: string +} + +export interface EditingFile { + name: string + path: string +} + +export type FileSortField = 'name' | 'size' | 'created' | 'modified' + +export type FileViewFilter = 'all' | 'filesOnly' | 'foldersOnly' + +export interface FileOperation { + id?: string + op: string + src: string + state: string + progress?: number + bytes_processed?: number + files_processed?: number + current_file?: string +} + +export interface UndoableOperation { + type: 'move' | 'rename' + itemType: string + fileName: string +} + +export interface MoveOperation extends UndoableOperation { + type: 'move' + sourcePath: string + destinationPath: string +} + +export interface RenameOperation extends UndoableOperation { + type: 'rename' + path: string + oldName: string + newName: string +} + +export type Operation = MoveOperation | RenameOperation + +export interface ExtractDryRunResult { + modpack_name: string | null + conflicting_files: string[] +} + +export interface FileUploadHandle { + promise: Promise + cancel: () => void + onProgress: (cb: (progress: { progress: number; loaded: number; total: number }) => void) => void +} diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/files.vue b/packages/ui/src/layouts/wrapped/hosting/manage/files.vue index 66fbed5866..9d503c0bc9 100644 --- a/packages/ui/src/layouts/wrapped/hosting/manage/files.vue +++ b/packages/ui/src/layouts/wrapped/hosting/manage/files.vue @@ -1,327 +1,35 @@ - - - + From d78155147e3778cf39cc94ba3108aa96488d9d73 Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Thu, 19 Mar 2026 17:43:41 +0000 Subject: [PATCH 03/49] fix: qa --- .../files-tab/components/FileNavbar.vue | 2 +- .../files-tab/components/FileTableRow.vue | 2 +- .../files-tab/components/FileVirtualList.vue | 2 +- .../modals/FileUploadZipUrlModal.vue | 95 ++++++++----------- .../layouts/wrapped/hosting/manage/files.vue | 3 +- 5 files changed, 42 insertions(+), 62 deletions(-) diff --git a/packages/ui/src/layouts/shared/files-tab/components/FileNavbar.vue b/packages/ui/src/layouts/shared/files-tab/components/FileNavbar.vue index 40960738ae..edce45cd9e 100644 --- a/packages/ui/src/layouts/shared/files-tab/components/FileNavbar.vue +++ b/packages/ui/src/layouts/shared/files-tab/components/FileNavbar.vue @@ -57,7 +57,7 @@
-
  • +
  • {{ editingFileName }} diff --git a/packages/ui/src/layouts/shared/files-tab/components/FileTableRow.vue b/packages/ui/src/layouts/shared/files-tab/components/FileTableRow.vue index 42b310e456..0fc7d7b7b0 100644 --- a/packages/ui/src/layouts/shared/files-tab/components/FileTableRow.vue +++ b/packages/ui/src/layouts/shared/files-tab/components/FileTableRow.vue @@ -132,7 +132,7 @@ const formatDateTime = useFormatDateTime({ }) const containerClasses = computed(() => [ - 'group m-0 flex h-[74px] w-full select-none items-center justify-between overflow-hidden border-0 border-t border-solid border-surface-4 px-3 focus:!outline-none', + 'group m-0 flex w-full select-none items-center justify-between overflow-hidden border-0 border-t border-solid border-surface-4 px-3 py-3 focus:!outline-none', props.selected ? 'bg-surface-2.5' : props.index % 2 === 0 ? 'bg-surface-2' : 'bg-surface-1.5', props.isLast ? 'rounded-b-[20px]' : '', isEditableFile.value ? 'cursor-pointer' : props.type === 'directory' ? 'cursor-pointer' : '', diff --git a/packages/ui/src/layouts/shared/files-tab/components/FileVirtualList.vue b/packages/ui/src/layouts/shared/files-tab/components/FileVirtualList.vue index 3807ceebf3..34188b71d5 100644 --- a/packages/ui/src/layouts/shared/files-tab/components/FileVirtualList.vue +++ b/packages/ui/src/layouts/shared/files-tab/components/FileVirtualList.vue @@ -81,7 +81,7 @@ const emit = defineEmits<{ const { listContainer, totalHeight, visibleRange, visibleTop, visibleItems } = useVirtualScroll( toRef(props, 'items'), { - itemHeight: 74, + itemHeight: 53, bufferSize: 5, onNearEnd: () => emit('loadMore'), }, diff --git a/packages/ui/src/layouts/shared/files-tab/components/modals/FileUploadZipUrlModal.vue b/packages/ui/src/layouts/shared/files-tab/components/modals/FileUploadZipUrlModal.vue index 3c2cbec3d4..8f1f4994c5 100644 --- a/packages/ui/src/layouts/shared/files-tab/components/modals/FileUploadZipUrlModal.vue +++ b/packages/ui/src/layouts/shared/files-tab/components/modals/FileUploadZipUrlModal.vue @@ -1,47 +1,34 @@ + {{ ctx.downloadButtonLabel ?? formatMessage(commonMessages.downloadButton) }} -
    - +
    +
    +
    @@ -183,13 +202,15 @@ import { TrashIcon, XIcon, } from '@modrinth/assets' -import { computed, onMounted, onUnmounted, ref, watch } from 'vue' +import type { Component } from 'vue' +import { computed, onMounted, onUnmounted, ref, shallowRef, watch } from 'vue' import Admonition from '#ui/components/base/Admonition.vue' import ButtonStyled from '#ui/components/base/ButtonStyled.vue' import FloatingActionBar from '#ui/components/base/FloatingActionBar.vue' import { defineMessages, useVIntl } from '#ui/composables/i18n' import { useStickyObserver } from '#ui/composables/sticky-observer' +import { useVirtualScroll } from '#ui/composables/virtual-scroll' import { injectNotificationManager } from '#ui/providers/web-notifications' import { commonMessages } from '#ui/utils/common-messages' import { getFileExtension } from '#ui/utils/file-extensions' @@ -200,7 +221,7 @@ import FileManagerError from './components/FileManagerError.vue' import FileNavbar from './components/FileNavbar.vue' import FileOperationAdmonitions from './components/FileOperationAdmonitions.vue' import FileTableHeader from './components/FileTableHeader.vue' -import FileVirtualList from './components/FileVirtualList.vue' +import FileTableRow from './components/FileTableRow.vue' import FileCreateItemModal from './components/modals/FileCreateItemModal.vue' import FileDeleteItemModal from './components/modals/FileDeleteItemModal.vue' import FileMoveItemModal from './components/modals/FileMoveItemModal.vue' @@ -284,6 +305,12 @@ defineProps<{ const { addNotification } = injectNotificationManager() const ctx = injectFileManager() +const editorComponent = shallowRef(null) +import('vue3-ace-editor').then(async (mod) => { + await import('#ui/utils/ace-theme') + editorComponent.value = mod.VAceEditor +}) + const baseId = `files-${Math.random().toString(36).slice(2, 9)}` const items = computed(() => ctx.items.value) @@ -325,6 +352,15 @@ const { recordOperation, onKeydown } = useFileUndoRedo( (title, text, type) => addNotification({ title, text, type }), ) +// Virtual scroll +const { listContainer: virtualListContainer, totalHeight, visibleRange, visibleTop, visibleItems } = useVirtualScroll( + filteredItems, + { + itemHeight: 61, + bufferSize: 5, + }, +) + // Sticky observer for the table header const fileUploadRef = ref>() const fileUploadEl = computed(() => fileUploadRef.value?.$el as HTMLElement | null) diff --git a/packages/ui/src/layouts/shared/files-tab/providers/file-manager.ts b/packages/ui/src/layouts/shared/files-tab/providers/file-manager.ts index 4d4ac2d81b..ec9a699b49 100644 --- a/packages/ui/src/layouts/shared/files-tab/providers/file-manager.ts +++ b/packages/ui/src/layouts/shared/files-tab/providers/file-manager.ts @@ -1,4 +1,4 @@ -import type { Component, ComputedRef, Ref } from 'vue' +import type { ComputedRef, Ref } from 'vue' import { createContext } from '#ui/providers/create-context' @@ -63,8 +63,12 @@ export interface FileManagerContext { prefetchDirectory?: (path: string) => void prefetchFile?: (path: string) => void - // === Editor (provider supplies the lazy-loaded component) === - editorComponent: Ref + // === Feature flags === + showInstallFromUrl?: boolean + + // === Label overrides === + downloadButtonLabel?: string + uploadingLabel?: (completed: number, total: number) => string // === Optional capabilities === canRestart?: boolean diff --git a/packages/ui/src/layouts/shared/files-tab/types.ts b/packages/ui/src/layouts/shared/files-tab/types.ts index a3c1252d6d..12416aa5e0 100644 --- a/packages/ui/src/layouts/shared/files-tab/types.ts +++ b/packages/ui/src/layouts/shared/files-tab/types.ts @@ -66,12 +66,4 @@ export interface ExtractDryRunResult { conflicting_files: string[] } -export interface UploadState { - isUploading: boolean - currentFileName: string | null - currentFileProgress: number - uploadedBytes: number - totalBytes: number - completedFiles: number - totalFiles: number -} +export type { UploadState } from '@modrinth/api-client' diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/files.vue b/packages/ui/src/layouts/wrapped/hosting/manage/files.vue index ac03bb2cf9..59319f2ae5 100644 --- a/packages/ui/src/layouts/wrapped/hosting/manage/files.vue +++ b/packages/ui/src/layouts/wrapped/hosting/manage/files.vue @@ -1,7 +1,7 @@ @@ -319,12 +320,14 @@ import { MIN_SUMMARY_CHARS } from '@modrinth/moderation' import { Avatar, Combobox, + ConfirmLeaveModal, ConfirmModal, injectModrinthClient, injectNotificationManager, injectProjectPageContext, StyledInput, UnsavedChangesPopup, + usePageLeaveSafety, } from '@modrinth/ui' import { fileIsValid, formatProjectStatus, formatProjectType } from '@modrinth/utils' @@ -480,6 +483,14 @@ const modified = computed(() => ({ deletedBanner: deletedBanner.value, })) +const hasChanges = computed(() => + Object.keys(modified.value).some( + (key) => original.value[key] !== modified.value[key], + ), +) + +const { confirmLeaveModal } = usePageLeaveSafety(hasChanges) + function resetChanges() { name.value = project.value.title slug.value = project.value.slug diff --git a/apps/frontend/src/pages/[type]/[id]/settings/license.vue b/apps/frontend/src/pages/[type]/[id]/settings/license.vue index 4a18ed2201..4ea8cef9af 100644 --- a/apps/frontend/src/pages/[type]/[id]/settings/license.vue +++ b/apps/frontend/src/pages/[type]/[id]/settings/license.vue @@ -1,5 +1,6 @@ @@ -195,7 +171,6 @@ import { LinkIcon, PlusIcon, RefreshCwIcon, - SaveIcon, SearchIcon, ShareIcon, UploadIcon, @@ -206,7 +181,6 @@ import Button from '#ui/components/base/Button.vue' import ButtonStyled from '#ui/components/base/ButtonStyled.vue' import OverflowMenu from '#ui/components/base/OverflowMenu.vue' import StyledInput from '#ui/components/base/StyledInput.vue' -import TeleportOverflowMenu from '#ui/components/base/TeleportOverflowMenu.vue' import { defineMessages, useVIntl } from '#ui/composables/i18n' import { commonMessages } from '#ui/utils/common-messages' @@ -265,22 +239,6 @@ const messages = defineMessages({ id: 'files.navbar.share-to-mclogs', defaultMessage: 'Share to mclo.gs', }, - saveFile: { - id: 'files.navbar.save-file', - defaultMessage: 'Save file', - }, - save: { - id: 'files.navbar.save', - defaultMessage: 'Save', - }, - saveAs: { - id: 'files.navbar.save-as', - defaultMessage: 'Save as...', - }, - saveAndRestart: { - id: 'files.navbar.save-and-restart', - defaultMessage: 'Save & restart', - }, }) const props = defineProps<{ @@ -307,9 +265,6 @@ defineEmits<{ uploadZip: [] unzipFromUrl: [cf: boolean] refresh: [] - save: [] - saveAs: [] - saveRestart: [] share: [] }>() diff --git a/packages/ui/src/layouts/shared/files-tab/components/editor/FileEditor.vue b/packages/ui/src/layouts/shared/files-tab/components/editor/FileEditor.vue index 0dea095ac4..e3f4ada2c9 100644 --- a/packages/ui/src/layouts/shared/files-tab/components/editor/FileEditor.vue +++ b/packages/ui/src/layouts/shared/files-tab/components/editor/FileEditor.vue @@ -81,14 +81,6 @@ const messages = defineMessages({ id: 'files.editor.save-failed-text', defaultMessage: 'Could not save the file.', }, - serverRestartedTitle: { - id: 'files.editor.server-restarted-title', - defaultMessage: 'Server restarted', - }, - serverRestartedText: { - id: 'files.editor.server-restarted-text', - defaultMessage: 'Your server has been restarted.', - }, logUrlCopiedTitle: { id: 'files.editor.log-url-copied-title', defaultMessage: 'Log URL copied', @@ -108,6 +100,7 @@ const messages = defineMessages({ }) const fileContent = ref('') +const originalContent = ref('') const isEditingImage = ref(false) const imagePreview = ref(null) const isLoading = ref(false) @@ -159,7 +152,9 @@ async function loadFileContent(file: { name: string; path: string }) { imagePreview.value = content } else { isEditingImage.value = false - fileContent.value = await ctx.readFile(normalizedPath) + const content = await ctx.readFile(normalizedPath) + fileContent.value = content + originalContent.value = content } } catch (error) { console.error('Error fetching file content:', error) @@ -174,8 +169,17 @@ async function loadFileContent(file: { name: string; path: string }) { } } +const hasUnsavedChanges = computed( + () => !isEditingImage.value && !isLoading.value && fileContent.value !== originalContent.value, +) + +function revertChanges() { + fileContent.value = originalContent.value +} + function resetState() { fileContent.value = '' + originalContent.value = '' isEditingImage.value = false imagePreview.value = null } @@ -198,13 +202,15 @@ function onEditorInit(editor: { }) } -async function saveFileContent(exit: boolean = true) { +async function saveFileContent(exit: boolean = false) { if (!props.file) return try { const normalizedPath = props.file.path.startsWith('/') ? props.file.path : `/${props.file.path}` await ctx.writeFile(normalizedPath, fileContent.value) + originalContent.value = fileContent.value + if (exit) { emit('close') } @@ -224,21 +230,6 @@ async function saveFileContent(exit: boolean = true) { } } -async function saveAndRestart() { - await saveFileContent(false) - - if (ctx.restartServer) { - await ctx.restartServer() - addNotification({ - title: formatMessage(messages.serverRestartedTitle), - text: formatMessage(messages.serverRestartedText), - type: 'success', - }) - } - - emit('close') -} - async function shareToMclogs() { if (ctx.shareToMclogs) { await ctx.shareToMclogs(fileContent.value) @@ -287,10 +278,11 @@ onUnmounted(() => { defineExpose({ saveFileContent, - saveAndRestart, shareToMclogs, close, isEditingImage, fileContent, + hasUnsavedChanges, + revertChanges, }) diff --git a/packages/ui/src/layouts/shared/files-tab/layout.vue b/packages/ui/src/layouts/shared/files-tab/layout.vue index 6b808d3dcf..56285108f0 100644 --- a/packages/ui/src/layouts/shared/files-tab/layout.vue +++ b/packages/ui/src/layouts/shared/files-tab/layout.vue @@ -1,5 +1,12 @@ From ef216a6d96701d2ef00fba35589a9bd2ecdcb7d8 Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Sat, 21 Mar 2026 12:49:56 +0000 Subject: [PATCH 23/49] fix: lint --- .../src/pages/[type]/[id]/settings/index.vue | 4 +- .../pages/[type]/[id]/settings/license.vue | 1 - .../components/modal/ConfirmLeaveModal.vue | 16 +---- .../src/layouts/shared/content-tab/index.ts | 2 +- .../src/layouts/shared/files-tab/layout.vue | 5 +- .../shared/installation-settings/layout.vue | 2 +- .../wrapped/hosting/manage/content.vue | 2 +- packages/ui/src/locales/en-US/index.json | 60 ++++++++++--------- 8 files changed, 42 insertions(+), 50 deletions(-) diff --git a/apps/frontend/src/pages/[type]/[id]/settings/index.vue b/apps/frontend/src/pages/[type]/[id]/settings/index.vue index b6fb7150bb..e78a94564d 100644 --- a/apps/frontend/src/pages/[type]/[id]/settings/index.vue +++ b/apps/frontend/src/pages/[type]/[id]/settings/index.vue @@ -484,9 +484,7 @@ const modified = computed(() => ({ })) const hasChanges = computed(() => - Object.keys(modified.value).some( - (key) => original.value[key] !== modified.value[key], - ), + Object.keys(modified.value).some((key) => original.value[key] !== modified.value[key]), ) const { confirmLeaveModal } = usePageLeaveSafety(hasChanges) diff --git a/apps/frontend/src/pages/[type]/[id]/settings/license.vue b/apps/frontend/src/pages/[type]/[id]/settings/license.vue index 4ea8cef9af..682b60ecf4 100644 --- a/apps/frontend/src/pages/[type]/[id]/settings/license.vue +++ b/apps/frontend/src/pages/[type]/[id]/settings/license.vue @@ -173,7 +173,6 @@ import { computed } from 'vue' const { projectV2: project, currentMember, patchProject } = injectProjectPageContext() - function getInitialLicense() { const oldLicenseId = project.value.license.id const trimmedLicenseId = oldLicenseId diff --git a/packages/ui/src/components/modal/ConfirmLeaveModal.vue b/packages/ui/src/components/modal/ConfirmLeaveModal.vue index 6d54259e61..b1ee34ff7b 100644 --- a/packages/ui/src/components/modal/ConfirmLeaveModal.vue +++ b/packages/ui/src/components/modal/ConfirmLeaveModal.vue @@ -1,10 +1,5 @@ { .sort(([, a], [, b]) => b - a) .map(([type]) => { const msg = - commonProjectTypeCategoryMessages[ - type as keyof typeof commonProjectTypeCategoryMessages - ] + commonProjectTypeCategoryMessages[type as keyof typeof commonProjectTypeCategoryMessages] return { id: type, label: msg ? formatMessage(msg) : type.charAt(0).toUpperCase() + type.slice(1) + 's', @@ -178,7 +176,9 @@ function toggleFilter(filterId: string) { const typeFilteredCount = computed(() => { if (selectedFilters.value.length === 0) return items.value.length - return items.value.filter((item) => selectedFilters.value.includes(normalizeProjectType(item.project_type))).length + return items.value.filter((item) => + selectedFilters.value.includes(normalizeProjectType(item.project_type)), + ).length }) const filteredItems = computed(() => { @@ -197,7 +197,9 @@ const filteredItems = computed(() => { // Apply type filters if (selectedFilters.value.length > 0) { - result = result.filter((item) => selectedFilters.value.includes(normalizeProjectType(item.project_type))) + result = result.filter((item) => + selectedFilters.value.includes(normalizeProjectType(item.project_type)), + ) } return result @@ -486,7 +488,17 @@ defineExpose({ show, showLoading, hide, getState, restore, updateItem })
    - {{ count }} {{ formatMessage(commonProjectTypeTitleMessages[normalizeProjectType(type as string) as keyof typeof commonProjectTypeTitleMessages] ?? commonProjectTypeTitleMessages.project, { count }) }} + {{ count }} + {{ + formatMessage( + commonProjectTypeTitleMessages[ + normalizeProjectType( + type as string, + ) as keyof typeof commonProjectTypeTitleMessages + ] ?? commonProjectTypeTitleMessages.project, + { count }, + ) + }}
    diff --git a/packages/ui/src/layouts/shared/content-tab/composables/content-filtering.ts b/packages/ui/src/layouts/shared/content-tab/composables/content-filtering.ts index 2841ff8df2..7f448e1e18 100644 --- a/packages/ui/src/layouts/shared/content-tab/composables/content-filtering.ts +++ b/packages/ui/src/layouts/shared/content-tab/composables/content-filtering.ts @@ -3,10 +3,7 @@ import type { Ref } from 'vue' import { computed, ref, watch } from 'vue' import { useVIntl } from '#ui/composables/i18n' -import { - commonProjectTypeCategoryMessages, - normalizeProjectType, -} from '#ui/utils/common-messages' +import { commonProjectTypeCategoryMessages, normalizeProjectType } from '#ui/utils/common-messages' import type { ContentItem } from '../types' @@ -48,9 +45,7 @@ export function useContentFilters(items: Ref, config?: ContentFil const types = Object.keys(frequency).sort((a, b) => frequency[b] - frequency[a]) for (const type of types) { const msg = - commonProjectTypeCategoryMessages[ - type as keyof typeof commonProjectTypeCategoryMessages - ] + commonProjectTypeCategoryMessages[type as keyof typeof commonProjectTypeCategoryMessages] const label = msg ? formatMessage(msg) : type.charAt(0).toUpperCase() + type.slice(1) + 's' options.push({ id: type, label }) } @@ -101,7 +96,10 @@ export function useContentFilters(items: Ref, config?: ContentFil const activeAttributes = selectedFilters.value.filter((f) => attributeFilters.has(f)) return source.filter((item) => { - if (typeFilters.length > 0 && !typeFilters.includes(normalizeProjectType(item.project_type))) { + if ( + typeFilters.length > 0 && + !typeFilters.includes(normalizeProjectType(item.project_type)) + ) { return false } diff --git a/packages/ui/src/layouts/shared/files-tab/components/FileNavbar.vue b/packages/ui/src/layouts/shared/files-tab/components/FileNavbar.vue index df0dcafafc..5fbc36278c 100644 --- a/packages/ui/src/layouts/shared/files-tab/components/FileNavbar.vue +++ b/packages/ui/src/layouts/shared/files-tab/components/FileNavbar.vue @@ -18,170 +18,178 @@ />
    - + +
  • + + -
    -
    -
    - -
    +
    + +
    @@ -358,9 +366,12 @@ onBeforeUnmount(() => { bcResizeObserver?.disconnect() }) -watch(() => props.breadcrumbs, () => { - requestAnimationFrame(checkBreadcrumbOverflow) -}) +watch( + () => props.breadcrumbs, + () => { + requestAnimationFrame(checkBreadcrumbOverflow) + }, +) const isLogFile = computed(() => { return ( From 4ea121e58f80dd26e0c316623eb584cba7bcd88c Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Mon, 23 Mar 2026 18:17:13 +0000 Subject: [PATCH 36/49] fix: qa --- apps/app-frontend/src/pages/instance/Mods.vue | 1 + .../components/ContentCardTable.vue | 2 +- .../components/modals/ModpackContentModal.vue | 20 ++++++++++++++++++- .../wrapped/hosting/manage/content.vue | 1 + 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/apps/app-frontend/src/pages/instance/Mods.vue b/apps/app-frontend/src/pages/instance/Mods.vue index dfc62c3ad2..4d6c98adba 100644 --- a/apps/app-frontend/src/pages/instance/Mods.vue +++ b/apps/app-frontend/src/pages/instance/Mods.vue @@ -13,6 +13,7 @@ :modpack-icon-url="linkedModpackProject?.icon_url ?? undefined" :enable-toggle="!props.isServerInstance" :get-overflow-options="getOverflowOptions" + :switch-version="handleSwitchVersion" @update:enabled="handleModpackContentToggle" @bulk:enable="handleModpackContentBulkToggle" @bulk:disable="handleModpackContentBulkToggle" diff --git a/packages/ui/src/layouts/shared/content-tab/components/ContentCardTable.vue b/packages/ui/src/layouts/shared/content-tab/components/ContentCardTable.vue index 32d386adaf..50b160afa0 100644 --- a/packages/ui/src/layouts/shared/content-tab/components/ContentCardTable.vue +++ b/packages/ui/src/layouts/shared/content-tab/components/ContentCardTable.vue @@ -297,7 +297,7 @@ function handleSort(column: ContentCardTableSortColumn) { @update:enabled="(val) => emit('update:enabled', item.id, val)" @delete="(e: MouseEvent) => emit('delete', item.id, e)" @update="emit('update', item.id)" - @switch-version="emit('switchVersion', item.id)" + v-on="hasSwitchVersionListener ? { 'switch-version': () => emit('switchVersion', item.id) } : {}" >