Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<script setup lang="ts">
import { Button, injectNotificationManager } from '@modrinth/ui'
import { computed, ref } from 'vue'
import type { ArchiveInput } from '../types'
import { validateArchive } from '../validation'
import JsonEditor from './JsonEditor.vue'
const json = defineModel<string>({ required: true })
const props = defineProps<{
editingId?: string
persist: (entries: ArchiveInput[]) => Promise<boolean>
saving: boolean
}>()
const emit = defineEmits<{ saved: []; cancel: [] }>()
const { addNotification } = injectNotificationManager()
const submitted = ref(false)
const parsedJson = computed(() => {
try {
const archive: unknown = JSON.parse(json.value)
const errors = validateArchive(archive)
if (errors.length) return { archive: null, error: errors.join(' ') }
const input = archive as ArchiveInput
if (input.id !== props.editingId) {
return {
archive: null,
error: props.editingId
? 'The archive ID must remain unchanged.'
: 'Omit the ID when adding a new archive.',
}
}
return { archive: input, error: '' }
} catch {
return { archive: null, error: 'Enter valid JSON for a single archive object.' }
}
})
async function save() {
submitted.value = true
if (props.saving) return
if (!parsedJson.value.archive) {
addNotification({
title: 'Invalid copyright archive',
text: parsedJson.value.error,
type: 'error',
})
return
}
if (await props.persist([parsedJson.value.archive])) emit('saved')
}
</script>

<template>
<div class="flex min-w-0 flex-col gap-3">
<JsonEditor
v-model="json"
label="Archive JSON"
:disabled="saving"
:error="submitted && !!parsedJson.error"
/>
<div class="flex flex-wrap items-center gap-2">
<slot name="actions-start" />
<div class="ml-auto flex gap-2">
<Button type="quiet" :disabled="saving" @click="emit('cancel')">Cancel</Button>
<Button type="colored" color="brand" :disabled="saving" @click="save">
{{ saving ? 'Saving...' : editingId ? 'Save changes' : 'Add archive' }}
</Button>
</div>
</div>
</div>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<script setup lang="ts">
import { NewModal } from '@modrinth/ui'
import { ref } from 'vue'

import type { ArchiveInput } from '../types'
import ArchiveJsonForm from './ArchiveJsonForm.vue'

defineProps<{
persist: (entries: ArchiveInput[]) => Promise<boolean>
saving: boolean
}>()
const modal = ref<InstanceType<typeof NewModal>>()
const json = ref('')
const formKey = ref(0)

function show() {
json.value = JSON.stringify({ name: '', license: '', files: [] }, null, '\t')
formKey.value++
modal.value?.show()
}

defineExpose({ show })
</script>

<template>
<NewModal
ref="modal"
header="New copyright archive"
width="860px"
max-width="calc(100vw - 2rem)"
:close-on-click-outside="false"
:disable-close="saving"
>
<ArchiveJsonForm
:key="formKey"
v-model="json"
:persist="persist"
:saving="saving"
@saved="modal?.hide()"
@cancel="modal?.hide()"
/>
</NewModal>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<script setup lang="ts">
import { Button, injectNotificationManager, NewModal } from '@modrinth/ui'
import { computed, ref } from 'vue'
import type { ArchiveInput } from '../types'
import { parseImport } from '../validation'
import JsonEditor from './JsonEditor.vue'
const props = defineProps<{
existingIds: string[]
persist: (entries: ArchiveInput[]) => Promise<boolean>
saving: boolean
}>()
const { addNotification } = injectNotificationManager()
const submitted = ref(false)
const modal = ref<InstanceType<typeof NewModal>>()
const json = ref('')
const preview = computed(() => {
if (!json.value.trim()) return { entries: [], error: '' }
try {
return { entries: parseImport(json.value, new Set(props.existingIds)), error: '' }
} catch (error) {
return { entries: [], error: error instanceof Error ? error.message : String(error) }
}
})
const replacements = computed(() => preview.value.entries.filter((entry) => entry.id).length)
function show() {
json.value = ''
submitted.value = false
modal.value?.show()
}
async function save() {
submitted.value = true
if (props.saving) return
if (!preview.value.entries.length || preview.value.error) {
addNotification({
title: 'Could not import copyright archives',
text: preview.value.error || 'Enter a JSON array of archives.',
type: 'error',
})
return
}
if (await props.persist(preview.value.entries)) modal.value?.hide()
}
defineExpose({ show })
</script>

<template>
<NewModal
ref="modal"
header="Bulk import copyright archives"
width="720px"
max-width="calc(100vw - 2rem)"
:close-on-click-outside="false"
:disable-close="saving"
>
<div class="flex flex-col gap-4">
<p class="m-0">
Paste a JSON array of archives. Omit IDs to create new archives. Include an existing ID to
replace its entire record.
</p>
<div class="flex flex-col gap-2">
<span class="font-semibold">Archives JSON</span>
<JsonEditor
v-model="json"
label="Archives JSON"
:disabled="saving"
:error="submitted && !!preview.error"
/>
</div>
<p v-if="preview.entries.length" role="status" class="m-0">
{{ preview.entries.length - replacements }} to create, {{ replacements }} to replace.
</p>
</div>
<template #actions>
<div class="flex justify-end gap-2">
<Button type="quiet" :disabled="saving" @click="modal?.hide()">Cancel</Button>
<Button type="colored" color="brand" :disabled="saving" @click="save">{{
saving ? 'Importing...' : 'Import archives'
}}</Button>
</div>
</template>
</NewModal>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<script setup lang="ts">
import { injectNotificationManager } from '@modrinth/ui'
import type { Ace } from 'ace-builds'
import { type Component, onMounted, ref, shallowRef } from 'vue'
const json = defineModel<string>({ required: true })
defineProps<{ disabled: boolean; label: string; error?: boolean }>()
const editorComponent = shallowRef<Component | null>(null)
const loadFailed = ref(false)
const { addNotification } = injectNotificationManager()
const options: Partial<Ace.EditorOptions> = {
useWorker: false,
tabSize: 4,
useSoftTabs: false,
showFoldWidgets: true,
highlightActiveLine: true,
}
onMounted(async () => {
try {
const [{ VAceEditor }] = await Promise.all([
import('vue3-ace-editor'),
import('@modrinth/ui/src/utils/ace-theme'),
])
await import('ace-builds/src-noconflict/mode-json')
editorComponent.value = VAceEditor
} catch {
loadFailed.value = true
addNotification({
title: 'Could not load the JSON editor',
text: 'Reload the page to try again.',
type: 'error',
})
}
})
</script>

<template>
<div
class="overflow-hidden rounded-xl border border-solid border-surface-4"
:class="{ '!border-red': error }"
>
<component
:is="editorComponent"
v-if="editorComponent"
v-model:value="json"
lang="json"
theme="modrinth"
:aria-label="label"
:readonly="disabled"
:print-margin="false"
:options="options"
:style="{ height: 'min(28rem, 55vh)', fontSize: '0.875rem' }"
class="ace-modrinth"
/>
<div v-else-if="loadFailed" class="p-4 text-secondary">JSON editor unavailable.</div>
<div v-else class="flex h-64 items-center justify-center text-secondary" role="status">
Loading JSON editor...
</div>
</div>
</template>
Loading
Loading