mirror of
https://github.com/modrinth/code.git
synced 2026-08-28 10:34:53 +00:00
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:
@@ -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)
|
||||
|
||||
+26
-4
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user