feat: hosting access tab (#5995)

* feat: implement access tab with dummy data

* fix: spacing

* feat: qa

* feat: implement backend

* qa: qa pass

* feat: fix user "search"

* fix: lint

* feat: change to bitfield

* feat: fix fields

* fix: lint

* fix: lint

* feat: hook up api

* feat: fix permissions

* feat: audit log table event start

* feat: better mobile mode for audit log table

* feat: i18n

* feat: qa

* feat: enforce permissions

* feat: email template start

* feat: qa

* fix: tooltip bug

* feat: qa

* impl: sse support in api-client

* feat: sse impl

* fix: desync path

* feat: time frame picker from analytics

* feat: QA

* fix: spacing

* fix: permisison audit log entries

* fix: hosting manage page shared server detection

* fix: lint

* feat: qa + lint

* feat: audit log table sort by time

* feat: finish frontend panel stuff

* fix: lint

* fix: backend alignment

* fix: lint

* fix: supress friend errors

* feat: qa

* fix: qa

* fix: lint

* fix: utils barrel

* fix: safari cookies in dev

* fix: pin nuxt

* feat: fixes + notif fix

* fix: notifications

* feat: qa

* fix: notification sync not happening immediately

* fix: qa

* fix: qa

* feat: qa

* blog + prepr

* feat: toast shit

* blog images

* thumbnail update one last time

* prepr

* feat: use reinvite route

* update images

* fix: reinvite stuff

* fix: lint

* fix: alignment of save bar

* fix: notif sizing

* fix: split up access

* fix: lint

* fix: lint

* fix: link

---------

Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
Calum H.
2026-06-04 15:58:01 +00:00
committed by GitHub
co-authored by Prospector
parent 58ad58f958
commit bd97ace974
227 changed files with 15578 additions and 2153 deletions
@@ -10,6 +10,7 @@
<ButtonStyled type="transparent" circular>
<button
v-tooltip="formatMessage(messages.toggleReplace)"
:disabled="props.readonly"
:aria-label="formatMessage(messages.toggleReplace)"
@click="toggleReplace"
>
@@ -88,6 +89,7 @@
type="search"
size="small"
autocomplete="off"
:disabled="props.readonly"
:placeholder="formatMessage(messages.replaceInFile)"
wrapper-class="w-44"
/>
@@ -95,7 +97,7 @@
<ButtonStyled type="outlined">
<button
class="!h-8 whitespace-nowrap px-2 text-sm disabled:opacity-50"
:disabled="findMatchCount === 0"
:disabled="props.readonly || findMatchCount === 0"
@click="emit('replace', replaceQuery)"
>
{{ formatMessage(messages.replace) }}
@@ -104,7 +106,7 @@
<ButtonStyled type="outlined">
<button
class="!h-8 whitespace-nowrap px-2 text-sm disabled:opacity-50"
:disabled="findMatchCount === 0"
:disabled="props.readonly || findMatchCount === 0"
@click="emit('replaceAll', replaceQuery)"
>
{{ formatMessage(messages.replaceAll) }}
@@ -129,6 +131,7 @@ const props = defineProps<{
findMatchCount: number
currentFindMatch: number
isEditingImage: boolean
readonly?: boolean
}>()
const emit = defineEmits<{
@@ -193,6 +196,7 @@ const findInputRef = ref<{ focus: () => void } | null>(null)
const replaceInputRef = ref<{ focus: () => void } | null>(null)
function toggleReplace() {
if (props.readonly) return
isReplaceOpen.value = !isReplaceOpen.value
if (isReplaceOpen.value) {
nextTick(() => replaceInputRef.value?.focus())
@@ -204,6 +208,7 @@ function focusFindInput() {
}
function openReplace() {
if (props.readonly) return
isReplaceOpen.value = true
nextTick(() => replaceInputRef.value?.focus())
}
@@ -8,6 +8,7 @@
v-model:is-find-open="isFindOpen"
v-model:find-query="inFileFindQuery"
:is-editing-image="isEditingImage"
:readonly="isEditorReadOnly"
:find-match-count="findMatchCount"
:current-find-match="currentFindMatch"
@find-next="findNext"
@@ -22,6 +23,7 @@
v-model:value="fileContent"
:lang="editorLanguage"
theme="modrinth"
:readonly="isEditorReadOnly"
:print-margin="false"
:style="{ height: editorHeight, fontSize: '0.875rem' }"
class="ace-modrinth rounded-[20px]"
@@ -144,6 +146,11 @@ const editorLanguage = computed(() => {
const ext = getFileExtension(props.file?.name ?? '')
return getEditorLanguage(ext)
})
const isEditorReadOnly = computed(() => ctx.isBusy?.value ?? false)
watch(isEditorReadOnly, (readOnly) => {
editorInstance.value?.setReadOnly(readOnly)
})
watch(
() => props.file,
@@ -206,6 +213,7 @@ function resetState() {
function onEditorInit(editor: Ace.Editor) {
editorInstance.value = editor
editor.setReadOnly(isEditorReadOnly.value)
editor.commands.addCommand({
name: 'save',
@@ -223,6 +231,7 @@ function onEditorInit(editor: Ace.Editor) {
name: 'replace',
bindKey: { win: 'Ctrl-H', mac: 'Command-Option-F' },
exec: () => {
if (isEditorReadOnly.value) return
isFindOpen.value = true
nextTick(() => findReplaceRef.value?.openReplace())
},
@@ -231,6 +240,7 @@ function onEditorInit(editor: Ace.Editor) {
async function saveFileContent(exit: boolean = false) {
if (!props.file) return
if (ctx.isBusy?.value) return
try {
const normalizedPath = props.file.path.startsWith('/') ? props.file.path : `/${props.file.path}`
@@ -312,7 +322,7 @@ function closeFind() {
function replaceOne(query: string) {
const editor = editorInstance.value
if (!editor || findMatchCount.value === 0) return
if (!editor || isEditorReadOnly.value || findMatchCount.value === 0) return
editor.replace(query)
nextTick(() => {
const count = countOccurrences(fileContent.value, inFileFindQuery.value)
@@ -323,7 +333,7 @@ function replaceOne(query: string) {
function replaceAllOccurrences(query: string) {
const editor = editorInstance.value
if (!editor || findMatchCount.value === 0) return
if (!editor || isEditorReadOnly.value || findMatchCount.value === 0) return
editor.replaceAll(query)
nextTick(() => {
const count = countOccurrences(fileContent.value, inFileFindQuery.value)
@@ -37,6 +37,7 @@
</div>
<StyledInput
v-model="url"
v-tooltip="props.disabled ? props.disabledTooltip : undefined"
:icon="LinkIcon"
type="url"
:placeholder="
@@ -44,7 +45,7 @@
? 'https://www.curseforge.com/minecraft/modpacks/.../files/6412259'
: 'https://www.example.com/.../modpack-name-1.0.2.zip'
"
:disabled="submitted"
:disabled="submitted || props.disabled"
:error="touched && !!error"
autocomplete="off"
@focus="touched = true"
@@ -74,8 +75,8 @@
</ButtonStyled>
<ButtonStyled color="brand">
<button
v-tooltip="error"
:disabled="submitted || !!error || backupInProgress"
v-tooltip="submitTooltip"
:disabled="submitDisabled"
type="submit"
@click="handleSubmit"
>
@@ -118,6 +119,17 @@ const { addNotification } = injectNotificationManager()
const client = injectModrinthClient()
const { formatMessage } = useVIntl()
const props = withDefaults(
defineProps<{
disabled?: boolean
disabledTooltip?: string
}>(),
{
disabled: false,
disabledTooltip: undefined,
},
)
const messages = defineMessages({
cfHeader: {
id: 'files.zip-url-modal.cf-header',
@@ -239,9 +251,17 @@ const error = computed(() => {
return ''
})
const submitDisabled = computed(
() => submitted.value || props.disabled || !!error.value || backupInProgress.value,
)
const submitTooltip = computed(() => {
if (props.disabled) return props.disabledTooltip
return error.value || undefined
})
const handleSubmit = async () => {
touched.value = true
if (error.value) return
if (submitDisabled.value) return
submitted.value = true
try {
@@ -270,6 +290,8 @@ const handleSubmit = async () => {
}
const show = (isCf: boolean) => {
if (props.disabled) return
cf.value = isCf
url.value = ''
submitted.value = false
@@ -3,7 +3,12 @@
<FileUnsavedChangesModal ref="unsavedChangesModal" />
<FileCreateItemModal ref="createItemModal" :type="newItemType" @create="handleCreateNewItem" />
<FileUploadConflictModal ref="uploadConflictModal" @proceed="handleExtractConfirm" />
<FileUploadZipUrlModal v-if="ctx.showInstallFromUrl" ref="uploadZipUrlModal" />
<FileUploadZipUrlModal
v-if="ctx.showInstallFromUrl"
ref="uploadZipUrlModal"
:disabled="isBusy"
:disabled-tooltip="busyTooltip"
/>
<FileRenameItemModal ref="renameItemModal" :item="selectedItem" @rename="handleRenameItem" />
<FileMoveItemModal
ref="moveItemModal"
@@ -156,7 +161,11 @@
</button>
</ButtonStyled>
<ButtonStyled color="brand">
<button @click="fileEditorRef?.saveFileContent(false)">
<button
v-tooltip="isBusy ? busyTooltip : undefined"
:disabled="isBusy"
@click="fileEditorRef?.saveFileContent(false)"
>
<SaveIcon /> {{ formatMessage(commonMessages.saveButton) }}
</button>
</ButtonStyled>
@@ -370,6 +379,7 @@ async function confirmDiscardChanges(): Promise<boolean> {
if (!hasUnsavedChanges.value) return true
const result = await unsavedChangesModal.value?.prompt()
if (result === 'save') {
if (isBusy.value) return false
await fileEditorRef.value?.saveFileContent(false)
return true
}
@@ -412,10 +422,12 @@ async function handleEditorClose() {
// CRUD handlers
async function handleCreateNewItem(name: string) {
if (isBusy.value) return
await ctx.createItem(name, newItemType.value)
}
async function handleRenameItem(newName: string) {
if (isBusy.value) return
const item = selectedItem.value
if (!item) return
@@ -432,6 +444,7 @@ async function handleRenameItem(newName: string) {
}
async function handleMoveItem(destination: string) {
if (isBusy.value) return
const item = selectedItem.value
if (!item) return
@@ -450,6 +463,7 @@ async function handleMoveItem(destination: string) {
}
function handleDeleteItem() {
if (isBusy.value) return
const item = selectedItem.value
if (!item) return
@@ -513,6 +527,7 @@ async function handleExtractItem(item: { name: string; type: string; path: strin
}
async function handleExtractConfirm(path: string) {
if (isBusy.value) return
if (!ctx.extractFile) return
try {
await ctx.extractFile(path, true, false)