Skip to content

Commit 17b3528

Browse files
authored
New Release (#97)
2 parents 63b2f50 + c82358b commit 17b3528

16 files changed

Lines changed: 653 additions & 53 deletions

File tree

src/assets/styles/navbar.css

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,23 @@
3636
height: 55px;
3737
}
3838

39+
.app-navbar-api-link {
40+
color: var(--bs-secondary-color);
41+
display: inline-flex;
42+
line-height: 1;
43+
}
44+
45+
.app-navbar-api-link:hover,
46+
.app-navbar-api-link:focus {
47+
color: var(--color-primary);
48+
}
49+
50+
.app-navbar-api-url {
51+
font-size: 0.8rem;
52+
overflow-wrap: anywhere;
53+
padding-block: 0.5rem;
54+
}
55+
3956
.app-navbar-panel {
4057
display: flex;
4158
flex-direction: column;

src/components/AppNavbar.vue

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import {
77
BNavbarNav,
88
BNavItem,
99
BOffcanvas,
10+
vBTooltip,
1011
} from "bootstrap-vue-next"
12+
import IconBoxArrowUpRight from "~icons/bi/box-arrow-up-right"
1113
import ThemeToggle from "./ThemeToggle.vue"
1214
import AccountMenu from "./AccountMenu.vue"
1315
import NavbarBrowseMenu from "./NavbarBrowseMenu.vue"
@@ -24,10 +26,12 @@ const theme = useThemeStore()
2426
const appName = computed(
2527
() => store.service?.prefLabel?.en ?? "JSKOS Server UI",
2628
)
29+
30+
const apiUrl = computed(() => store.service?.endpoint ?? store.activeUrl)
2731
</script>
2832
2933
<template>
30-
<BNavbar toggleable="md" class="app-navbar px-3 flex-nowrap">
34+
<BNavbar toggleable="lg" class="app-navbar px-3 flex-nowrap">
3135
<BNavbarBrand
3236
:to="{ name: 'overview' }"
3337
tag="router-link"
@@ -37,6 +41,18 @@ const appName = computed(
3741
{{ appName }}
3842
</BNavbarBrand>
3943
44+
<a
45+
v-if="apiUrl"
46+
v-b-tooltip.body="apiUrl"
47+
:href="apiUrl"
48+
target="_blank"
49+
rel="noopener"
50+
class="app-navbar-api-link flex-shrink-0 ms-2 me-auto"
51+
aria-label="Open API endpoint in a new tab"
52+
>
53+
<IconBoxArrowUpRight />
54+
</a>
55+
4056
<BNavbarToggle
4157
target="nav-offcanvas"
4258
class="flex-shrink-0"
@@ -50,6 +66,12 @@ const appName = computed(
5066
:title="appName"
5167
>
5268
<BNavbarNav>
69+
<li v-if="apiUrl" class="app-navbar-api-url">
70+
<a :href="apiUrl" target="_blank" rel="noopener">
71+
{{ apiUrl }}
72+
<IconBoxArrowUpRight />
73+
</a>
74+
</li>
5375
<BNavItem to="/" tag="router-link" @click="offcanvasVisible = false">
5476
Overview
5577
</BNavItem>
@@ -81,15 +103,15 @@ const appName = computed(
81103
</BOffcanvas>
82104
83105
<BNavbarNav
84-
class="gap-2 ms-auto mb-2 mb-md-0 d-none d-md-flex align-items-center"
106+
class="gap-2 ms-auto mb-2 mb-lg-0 d-none d-lg-flex align-items-center"
85107
>
86108
<BNavItem to="/" tag="router-link">Overview</BNavItem>
87109
<NavbarBrowseMenu />
88110
<BNavItem to="/import" tag="router-link">Import</BNavItem>
89111
<BNavItem to="/connection" tag="router-link">Connection</BNavItem>
90112
<BNavItem class="py-0">
91113
<div
92-
class="vr d-none d-md-flex opacity-50"
114+
class="vr d-none d-lg-flex opacity-50"
93115
style="height: 1.3rem"
94116
></div>
95117
</BNavItem>
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<script setup>
2+
import { BTooltip } from "bootstrap-vue-next"
3+
import IconInfoCircle from "~icons/bi/info-circle"
4+
5+
defineProps({
6+
label: {
7+
type: String,
8+
required: true,
9+
},
10+
})
11+
</script>
12+
13+
<template>
14+
<BTooltip class="import-option-info" placement="top">
15+
<template #target>
16+
<button
17+
type="button"
18+
class="import-option-info-trigger"
19+
:aria-label="`About ${label}`"
20+
>
21+
<IconInfoCircle aria-hidden="true" />
22+
</button>
23+
</template>
24+
<slot />
25+
</BTooltip>
26+
</template>
27+
28+
<style scoped>
29+
.import-option-info-trigger {
30+
display: inline-flex;
31+
padding: 0;
32+
border: 0;
33+
background: none;
34+
color: var(--bs-secondary-color);
35+
line-height: 1;
36+
}
37+
38+
.import-option-info-trigger:hover,
39+
.import-option-info-trigger:focus-visible {
40+
color: var(--bs-body-color);
41+
}
42+
</style>

src/components/import/ImportOptionRow.vue

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,32 @@
11
<script setup>
2+
import ImportOptionInfo from "@/components/import/ImportOptionInfo.vue"
3+
24
defineProps({
35
label: {
46
type: String,
57
required: true,
68
},
79
labelFor: {
810
type: String,
9-
required: true,
11+
default: null,
12+
},
13+
labelId: {
14+
type: String,
15+
default: null,
1016
},
1117
})
1218
</script>
1319

1420
<template>
1521
<div class="import-option-row">
16-
<div>
17-
<label class="import-option-label" :for="labelFor">{{ label }}</label>
18-
<p class="import-option-description"><slot name="description" /></p>
22+
<div class="import-option-heading">
23+
<label v-if="labelFor" class="import-option-label" :for="labelFor">{{
24+
label
25+
}}</label>
26+
<span v-else :id="labelId" class="import-option-label">{{ label }}</span>
27+
<ImportOptionInfo :label="label"
28+
><slot name="description"
29+
/></ImportOptionInfo>
1930
</div>
2031
<div class="import-option-control">
2132
<slot />
@@ -38,20 +49,19 @@ defineProps({
3849
@media (min-width: 768px) {
3950
.import-option-row {
4051
grid-template-columns: minmax(0, 1fr) minmax(0, 22rem);
41-
align-items: start;
52+
align-items: center;
4253
gap: 1.5rem;
4354
}
4455
}
4556
46-
.import-option-label {
47-
display: block;
48-
font-weight: 600;
57+
.import-option-heading {
58+
display: flex;
59+
align-items: center;
60+
gap: 0.375rem;
4961
}
5062
51-
.import-option-description {
52-
margin-bottom: 0;
53-
font-size: 0.875rem;
54-
color: var(--bs-secondary-color);
63+
.import-option-label {
64+
font-weight: 600;
5565
}
5666
5767
.import-option-control {

src/composables/useImport.js

Lines changed: 63 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import { useNotify } from "@/composables/useNotify"
44
import { useTypeAccess } from "@/composables/useTypeAccess"
55
import { getObjectType } from "@/utils/objectTypes"
66
import {
7+
ACCEPTED_FILE_TYPES_HINT,
78
IMPORTABLE_TYPES,
9+
SCHEME_MODES,
10+
SSSOM_CONTENT_TYPE,
811
detectFormat,
912
extractFirstObject,
1013
guessType,
@@ -52,6 +55,7 @@ export function useImport() {
5255

5356
const source = ref(null)
5457
const isBulk = ref(false)
58+
const schemeMode = ref(SCHEME_MODES[0].value)
5559
const selectedType = ref(null)
5660

5761
let abortController = null
@@ -103,6 +107,12 @@ export function useImport() {
103107

104108
const isUploading = computed(() => source.value?.status === "uploading")
105109

110+
/**
111+
* Whether the source is an SSSOM/TSV file, the only format whose import the
112+
* scheme handling applies to.
113+
*/
114+
const hasSchemeOptions = computed(() => source.value?.format === "sssom")
115+
106116
/**
107117
* What a finished import wrote, or null while none succeeded.
108118
*/
@@ -143,14 +153,19 @@ export function useImport() {
143153
if (inspected.kind !== "file") {
144154
return
145155
}
146-
try {
147-
const sample = await readSample(inspected.file, inspected.format)
148-
inspected.detectedType = guessType(
149-
extractFirstObject(sample, inspected.format),
150-
)
151-
} catch (error) {
152-
notify(`Could not read ${inspected.name}: ${error.message}`, "warning")
153-
return
156+
if (inspected.format === "sssom") {
157+
// SSSOM/TSV holds mappings by definition, and is not JSON to sample.
158+
inspected.detectedType = "mappings"
159+
} else {
160+
try {
161+
const sample = await readSample(inspected.file, inspected.format)
162+
inspected.detectedType = guessType(
163+
extractFirstObject(sample, inspected.format),
164+
)
165+
} catch (error) {
166+
notify(`Could not read ${inspected.name}: ${error.message}`, "warning")
167+
return
168+
}
154169
}
155170
if (
156171
inspected.detectedType &&
@@ -185,6 +200,13 @@ export function useImport() {
185200
if (!trimmed) {
186201
return
187202
}
203+
if (detectFormat(trimmed) === "sssom") {
204+
notify(
205+
`Cannot import ${trimmed}. The server cannot fetch SSSOM/TSV, so it has to be imported as a file.`,
206+
"warning",
207+
)
208+
return
209+
}
188210
setSource(createSource({ kind: "url", url: trimmed }))
189211
}
190212

@@ -216,7 +238,7 @@ export function useImport() {
216238
*/
217239
function reportRejectedFile(name) {
218240
notify(
219-
`Cannot import ${name} — only JSON and NDJSON are supported.`,
241+
`Cannot import ${name}. Only ${ACCEPTED_FILE_TYPES_HINT} are supported.`,
220242
"warning",
221243
)
222244
}
@@ -231,8 +253,8 @@ export function useImport() {
231253
/**
232254
* Builds the request parameters.
233255
*
234-
* A URL is handed to the server, which fetches it itself.
235-
* A file is streamed as multipart form data.
256+
* A URL is handed to the server, which fetches it itself. A file is sent as
257+
* the request body, see {@link buildBody}.
236258
*
237259
* @param {!ImportSource} imported The source to import.
238260
* @returns {!Object} The request parameters.
@@ -242,6 +264,9 @@ export function useImport() {
242264
if (isBulk.value) {
243265
params.bulk = true
244266
}
267+
if (hasSchemeOptions.value) {
268+
params.scheme = schemeMode.value
269+
}
245270
if (imported.kind === "url") {
246271
params.url = imported.url
247272
if (imported.format) {
@@ -254,18 +279,40 @@ export function useImport() {
254279
/**
255280
* Builds the request body.
256281
*
282+
* SSSOM/TSV is sent raw, because the server recognizes it by the request's
283+
* content type. Other formats are streamed as multipart form data.
284+
*
257285
* @param {!ImportSource} imported The source to import.
258-
* @returns {?FormData} The body, or null when the server fetches the data.
286+
* @returns {?FormData|!File} The body, or null when the server fetches the
287+
* data.
259288
*/
260289
function buildBody(imported) {
261290
if (imported.kind !== "file") {
262291
return null
263292
}
293+
if (imported.format === "sssom") {
294+
return imported.file
295+
}
264296
const body = new FormData()
265297
body.append("data", imported.file, imported.file.name)
266298
return body
267299
}
268300

301+
/**
302+
* Builds the request headers.
303+
*
304+
* @param {!ImportSource} imported The source to import.
305+
* @returns {!Object<string, string>} The headers, empty unless the format has
306+
* to be declared. Multipart bodies are left to axios, which adds the
307+
* boundary to their content type.
308+
*/
309+
function buildHeaders(imported) {
310+
if (imported.kind !== "file" || imported.format !== "sssom") {
311+
return {}
312+
}
313+
return { "Content-Type": SSSOM_CONTENT_TYPE }
314+
}
315+
269316
/**
270317
* Aborts a running import.
271318
*/
@@ -304,6 +351,7 @@ export function useImport() {
304351
method: "post",
305352
url: resolveImportUrl(store.activeUrl, type),
306353
params: buildParams(imported),
354+
headers: buildHeaders(imported),
307355
data: buildBody(imported),
308356
signal: abortController.signal,
309357
})
@@ -321,7 +369,7 @@ export function useImport() {
321369
imported.errorKind = "canceled"
322370
return
323371
}
324-
const { kind, message } = describeImportError(error)
372+
const { kind, message } = describeImportError(error, imported.format)
325373
imported.status = "failed"
326374
imported.errorKind = kind
327375
imported.error = message
@@ -354,12 +402,14 @@ export function useImport() {
354402
return {
355403
source,
356404
isBulk,
405+
schemeMode,
357406
selectedType,
358407
canImport,
359408
typeAccess,
360409
unavailableReason,
361410
blockedReason,
362411
hasTypeMismatch,
412+
hasSchemeOptions,
363413
isUploading,
364414
result,
365415
addFile,

src/stores/server.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ export const useServerStore = defineStore("server", () => {
108108
version: cfg.serverVersion,
109109
API_VERSION: cfg.version,
110110
api: "http://bartoc.org/api-type/jskos",
111-
endpoint: cfg.baseUrl ?? activeUrl,
111+
endpoint: cfg.baseUrl ?? activeUrl.value,
112112
ENV: cfg.env,
113113
AUTH: cfg.auth === true,
114114
CAPABILITIES: capabilities,

0 commit comments

Comments
 (0)