-
+
{{ $t("config.configuration.help") }}
@@ -27,13 +118,13 @@
:help="$t('config.format.help')"
/>
-
+
{{ $t("config.format.note.pdf") }}
-
-
+
+
-
-
diff --git a/src/corpus/config/Metadata.vue b/src/corpus/config/MetadataPanel.vue
similarity index 97%
rename from src/corpus/config/Metadata.vue
rename to src/corpus/config/MetadataPanel.vue
index 3c88e1f..bc9e0ec 100644
--- a/src/corpus/config/Metadata.vue
+++ b/src/corpus/config/MetadataPanel.vue
@@ -1,3 +1,15 @@
+
+
-
-
-
-
diff --git a/src/corpus/config/config.composable.js b/src/corpus/config/config.composable.ts
similarity index 72%
rename from src/corpus/config/config.composable.js
rename to src/corpus/config/config.composable.ts
index dda45e9..e7ac1c3 100644
--- a/src/corpus/config/config.composable.js
+++ b/src/corpus/config/config.composable.ts
@@ -1,10 +1,15 @@
import { computed } from "vue";
-import { emptyConfig, makeConfig, parseConfig } from "@/api/corpusConfig";
+import {
+ emptyConfig,
+ makeConfig,
+ parseConfig,
+ type ConfigOptions,
+} from "@/api/corpusConfig";
import useLocale from "@/i18n/locale.composable";
import useMinkBackend from "@/api/backend.composable";
import { useCorpusStore } from "@/store/corpus.store";
-export default function useConfig(corpusId) {
+export default function useConfig(corpusId: string) {
const corpusStore = useCorpusStore();
const { th } = useLocale();
const mink = useMinkBackend();
@@ -24,10 +29,10 @@ export default function useConfig(corpusId) {
corpus.value.config = config;
}
- async function uploadConfig(config, corpusId_ = corpusId) {
+ async function uploadConfig(config: ConfigOptions) {
// This may throw, either from makeConfig or saveConfig.
- await mink.saveConfig(corpusId_, await makeConfig(corpusId_, config));
- corpusStore.corpora[corpusId_].config = config;
+ await mink.saveConfig(corpusId, await makeConfig(corpusId, config));
+ corpusStore.corpora[corpusId].config = config;
}
return {
diff --git a/src/corpus/corpus.composable.js b/src/corpus/corpus.composable.js
deleted file mode 100644
index 8a0fd20..0000000
--- a/src/corpus/corpus.composable.js
+++ /dev/null
@@ -1,46 +0,0 @@
-import { useAuth } from "@/auth/auth.composable";
-import useMinkBackend from "@/api/backend.composable";
-import { useCorpusStore } from "@/store/corpus.store";
-import useMessenger from "@/message/messenger.composable";
-import useCorpora from "@/corpora/corpora.composable";
-import useConfig from "./config/config.composable";
-
-/** Let data be refreshed initially, but skip subsequent load calls. */
-const isCorpusFresh = {};
-
-export default function useCorpus(corpusId) {
- const corpusStore = useCorpusStore();
- const { refreshJwt } = useAuth();
- const mink = useMinkBackend();
- const { loadCorpora, refreshCorpora } = useCorpora();
- const { alertError } = useMessenger();
- const { loadConfig } = useConfig(corpusId);
-
- async function loadCorpus(force = false) {
- // Make sure the corpus has an entry in the store.
- await loadCorpora();
- if (isCorpusFresh[corpusId] && !force) {
- return;
- }
-
- // Load remaining essential info about the corpus.
- // Skip if removed.
- if (corpusId in corpusStore.corpora) {
- await loadConfig();
- }
-
- // Remember to skip loading next time.
- isCorpusFresh[corpusId] = true;
- }
-
- async function deleteCorpus(corpusId_ = corpusId) {
- // Delete corpus in the backend.
- await mink.deleteCorpus(corpusId_).catch(alertError);
- // The backend will have updated the remote JWT, so refresh our copy.
- // The backend uses the corpus list within it when listing available corpora.
- await refreshJwt();
- await refreshCorpora();
- }
-
- return { loadCorpus, deleteCorpus };
-}
diff --git a/src/corpus/corpus.composable.ts b/src/corpus/corpus.composable.ts
new file mode 100644
index 0000000..a96f62a
--- /dev/null
+++ b/src/corpus/corpus.composable.ts
@@ -0,0 +1,38 @@
+import { useCorpusStore } from "@/store/corpus.store";
+import useCorpora from "@/corpora/corpora.composable";
+import useConfig from "./config/config.composable";
+
+/** Let data be refreshed initially, but skip subsequent load calls. */
+const isCorpusFresh: Record
= {};
+
+export default function useCorpus(corpusId: string) {
+ const corpusStore = useCorpusStore();
+ const { loadCorpora } = useCorpora();
+ const { loadConfig } = useConfig(corpusId);
+
+ /**
+ * Load data about a corpus and store it.
+ */
+ async function loadCorpus(): Promise {
+ if (!corpusId) {
+ throw new RangeError("Corpus ID missing");
+ }
+
+ // Make sure the corpus has an entry in the store.
+ await loadCorpora();
+ if (isCorpusFresh[corpusId]) {
+ return;
+ }
+
+ // Load remaining essential info about the corpus.
+ // Skip if removed.
+ if (corpusId in corpusStore.corpora) {
+ await loadConfig();
+ }
+
+ // Remember to skip loading next time.
+ isCorpusFresh[corpusId] = true;
+ }
+
+ return { loadCorpus };
+}
diff --git a/src/corpus/corpusIdParam.composable.js b/src/corpus/corpusIdParam.composable.ts
similarity index 73%
rename from src/corpus/corpusIdParam.composable.js
rename to src/corpus/corpusIdParam.composable.ts
index 56ef971..544c277 100644
--- a/src/corpus/corpusIdParam.composable.js
+++ b/src/corpus/corpusIdParam.composable.ts
@@ -2,5 +2,5 @@ import { useRoute } from "vue-router";
export default function useCorpusIdParam() {
const route = useRoute();
- return route.params.corpusId;
+ return route.params.corpusId as string;
}
diff --git a/src/corpus/corpusState.composable.js b/src/corpus/corpusState.composable.ts
similarity index 64%
rename from src/corpus/corpusState.composable.js
rename to src/corpus/corpusState.composable.ts
index c3c8e5b..c9f41b8 100644
--- a/src/corpus/corpusState.composable.js
+++ b/src/corpus/corpusState.composable.ts
@@ -7,10 +7,10 @@ import useSources from "./sources/sources.composable";
import { getException } from "@/util";
/** The "corpus state" is related to the job status, but is more about predicting what action the user needs to take. */
-export function useCorpusState(corpusId) {
+export function useCorpusState(corpusId: string) {
const { sources } = useSources(corpusId);
const { config } = useConfig(corpusId);
- const { sparvStatus, korpStatus, strixStatus } = useJob(corpusId);
+ const { jobState } = useJob(corpusId);
const { t } = useI18n();
const corpusState = computed(() => {
@@ -18,58 +18,73 @@ export function useCorpusState(corpusId) {
if (!isConfigValid.value) return CorpusState.NEEDING_CONFIG;
if (!hasMetadata.value) return CorpusState.NEEDING_META;
- if (sparvStatus.value.isReady) return CorpusState.READY;
- if (sparvStatus.value.isError) return CorpusState.FAILED;
+ if (!jobState.value) {
+ console.warn(`Missing job state for ${corpusId}`);
+ return CorpusState.UNKNOWN;
+ }
- // TODO Revise the CorpusState concept. The workflow can now branch in two. Ifs below questionable.
- if (korpStatus.value.isRunning || strixStatus.value.isRunning)
+ if (jobState.value.sparv == "none" || jobState.value.sparv == "aborted")
+ return CorpusState.READY;
+ if (jobState.value.sparv == "error") return CorpusState.FAILED;
+
+ // TODO Revise the CorpusState concept. The workflow can now branch in two. Ifs below questionable.
+ if (
+ ["waiting", "running"].includes(jobState.value.korp) ||
+ ["waiting", "running"].includes(jobState.value.strix)
+ )
return CorpusState.RUNNING_INSTALL;
- if (sparvStatus.value.isRunning) return CorpusState.RUNNING;
- if (korpStatus.value.isReady && strixStatus.value.isReady)
+
+ if (jobState.value.sparv == "waiting" || jobState.value.sparv == "running")
+ return CorpusState.RUNNING;
+
+ if (
+ ["none", "aborted"].includes(jobState.value.korp) ||
+ ["none", "aborted"].includes(jobState.value.strix)
+ )
return CorpusState.DONE;
- if (korpStatus.value.isError || strixStatus.value.isError)
+
+ if (jobState.value.korp == "error" || jobState.value.strix == "error")
return CorpusState.FAILED_INSTALL;
- if (korpStatus.value.isDone || strixStatus.value.isDone)
+
+ if (jobState.value.korp == "done" || jobState.value.strix == "done")
return CorpusState.DONE_INSTALL;
- console.warn(
- `Invalid state, sparv=${sparvStatus.value.state}, korp=${korpStatus.value.state}, strix=${strixStatus.value.state}`
- );
+ console.warn("Invalid state", JSON.stringify(jobState.value));
return CorpusState.UNKNOWN;
});
const isConfigValid = computed(
- () => !getException(() => validateConfig(config.value))
+ () => config.value && !getException(() => validateConfig(config.value!)),
);
const hasMetadata = computed(
- () => config.value?.name?.swe || config.value?.name?.eng
+ () => config.value?.name?.swe || config.value?.name?.eng,
);
const isEmpty = computed(() => corpusState.value == CorpusState.EMPTY);
const isNeedingConfig = computed(
- () => corpusState.value == CorpusState.NEEDING_CONFIG
+ () => corpusState.value == CorpusState.NEEDING_CONFIG,
);
const isNeedingMeta = computed(
- () => corpusState.value == CorpusState.NEEDING_META
+ () => corpusState.value == CorpusState.NEEDING_META,
);
const canBeReady = computed(
- () => !isEmpty.value && !isNeedingConfig.value && !isNeedingMeta.value
+ () => !isEmpty.value && !isNeedingConfig.value && !isNeedingMeta.value,
);
const isFailed = computed(
() =>
corpusState.value == CorpusState.FAILED ||
- corpusState.value == CorpusState.FAILED_INSTALL
+ corpusState.value == CorpusState.FAILED_INSTALL,
);
const isReady = computed(() => corpusState.value == CorpusState.READY);
const isRunning = computed(() => corpusState.value == CorpusState.RUNNING);
const isDone = computed(() => corpusState.value == CorpusState.DONE);
const isRunningInstall = computed(
- () => corpusState.value == CorpusState.RUNNING_INSTALL
+ () => corpusState.value == CorpusState.RUNNING_INSTALL,
);
const isDoneInstall = computed(
- () => corpusState.value == CorpusState.DONE_INSTALL
+ () => corpusState.value == CorpusState.DONE_INSTALL,
);
const stateMessage = computed(() => t(`corpus.state.${corpusState.value}`));
@@ -79,7 +94,7 @@ export function useCorpusState(corpusId) {
isEmpty.value ||
isNeedingConfig.value ||
isNeedingMeta.value ||
- isFailed.value
+ isFailed.value,
);
return {
diff --git a/src/corpus/createCorpus.composable.js b/src/corpus/createCorpus.composable.ts
similarity index 55%
rename from src/corpus/createCorpus.composable.js
rename to src/corpus/createCorpus.composable.ts
index 14700d1..41aa25b 100644
--- a/src/corpus/createCorpus.composable.js
+++ b/src/corpus/createCorpus.composable.ts
@@ -3,39 +3,47 @@ import { useAuth } from "@/auth/auth.composable";
import useMinkBackend from "@/api/backend.composable";
import { useCorpusStore } from "@/store/corpus.store";
import useMessenger from "@/message/messenger.composable";
-import useConfig from "./config/config.composable";
-import useSources from "./sources/sources.composable";
-import useCorpus from "./corpus.composable";
+import useDeleteCorpus from "./deleteCorpus.composable";
import { getFilenameExtension } from "@/util";
+import {
+ makeConfig,
+ type FileFormat,
+ type ConfigOptions,
+} from "@/api/corpusConfig";
+import type { AxiosError } from "axios";
+import type { MinkResponse } from "@/api/api.types";
+import useCorpora from "@/corpora/corpora.composable";
export default function useCreateCorpus() {
const corpusStore = useCorpusStore();
const router = useRouter();
const { refreshJwt } = useAuth();
- const { deleteCorpus } = useCorpus();
- const { uploadConfig } = useConfig();
- const { uploadSources } = useSources();
+ const { deleteCorpus } = useDeleteCorpus();
const { alert, alertError } = useMessenger();
const mink = useMinkBackend();
+ const { refreshCorpora } = useCorpora();
async function createCorpus() {
const corpusId = await mink.createCorpus().catch(alertError);
+ if (!corpusId) return undefined;
// Have the new corpus included in further API calls.
await refreshJwt();
+ // Mark corpus store outdated. Instead of awaiting to have the ids present, add it manually below.
+ refreshCorpora();
// Adding the new id to store may trigger API calls, so do it after updating the JWT.
corpusStore.corpora[corpusId] = corpusStore.corpora[corpusId] || {};
return corpusId;
}
- async function createFromUpload(files) {
+ async function createFromUpload(files: FileList) {
const corpusId = await createCorpus().catch(alertError);
if (!corpusId) return;
// Get file extension of first file, assuming all are using the same extension.
- const format = getFilenameExtension(files[0]?.name);
+ const format = getFilenameExtension(files[0]?.name) as FileFormat;
// Create a minimal config.
- const config = {
+ const config: ConfigOptions = {
name: { swe: corpusId, eng: corpusId },
format,
};
@@ -46,7 +54,7 @@ export default function useCreateCorpus() {
]);
const rejectedResults = results.filter(
- (result) => result.status != "fulfilled"
+ (result): result is PromiseRejectedResult => result.status != "fulfilled",
);
if (rejectedResults.length) {
// Display error message(s).
@@ -59,7 +67,27 @@ export default function useCreateCorpus() {
router.push(`/corpus/${corpusId}`);
}
- async function createFromConfig(name, description, format, textAnnotation) {
+ // Like the `uploadConfig` in `config.composable.ts` but takes `corpusId` as argument.
+ async function uploadConfig(config: ConfigOptions, corpusId: string) {
+ // This may throw, either from makeConfig or saveConfig.
+ await mink.saveConfig(corpusId, await makeConfig(corpusId, config));
+ corpusStore.corpora[corpusId].config = config;
+ }
+
+ // Like the `uploadSources` in `sources.composable.ts` but takes `corpusId` as argument.
+ async function uploadSources(files: FileList, corpusId: string) {
+ await mink.uploadSources(corpusId, files);
+ const info = await mink.resourceInfoOne(corpusId).catch(alertError);
+ if (!info) return;
+ corpusStore.corpora[corpusId].sources = info.resource.source_files;
+ }
+
+ async function createFromConfig(
+ name: string,
+ description: string,
+ format: FileFormat,
+ textAnnotation?: string,
+ ): Promise {
const config = {
name: { swe: name, eng: name },
description: { swe: description, eng: description },
@@ -68,13 +96,8 @@ export default function useCreateCorpus() {
};
// Create an empty corpus. If it fails, abort.
- let corpusId;
- try {
- corpusId = await createCorpus().catch(alertError);
- } catch (e) {
- alertError(e);
- return;
- }
+ const corpusId = await createCorpus().catch(alertError);
+ if (!corpusId) return;
// Upload the basic config.
try {
@@ -84,9 +107,9 @@ export default function useCreateCorpus() {
return corpusId;
} catch (e) {
// If creating the config fails, there's a TypeError.
- if (e.name == "TypeError") alert(e.message, "error");
+ if (e instanceof TypeError) alert(e.message, "error");
// Otherwise it's probably a backend error when saving.
- else alertError(e);
+ else alertError(e as AxiosError);
// Discard the empty corpus.
await deleteCorpus(corpusId).catch(alertError);
}
diff --git a/src/corpus/deleteCorpus.composable.ts b/src/corpus/deleteCorpus.composable.ts
new file mode 100644
index 0000000..9df9491
--- /dev/null
+++ b/src/corpus/deleteCorpus.composable.ts
@@ -0,0 +1,31 @@
+import { useAuth } from "@/auth/auth.composable";
+import useMinkBackend from "@/api/backend.composable";
+import useMessenger from "@/message/messenger.composable";
+import useCorpora from "@/corpora/corpora.composable";
+
+export default function useDeleteCorpus() {
+ const { refreshJwt } = useAuth();
+ const mink = useMinkBackend();
+ const { refreshCorpora } = useCorpora();
+ const { alertError } = useMessenger();
+
+ /**
+ * Delete a corpus in backend.
+ *
+ * @param corpusId_ Needed if no id was set when calling `useCorpus`.
+ */
+ async function deleteCorpus(corpusId: string): Promise {
+ if (!corpusId) {
+ throw new RangeError("Corpus ID missing");
+ }
+
+ // Delete corpus in the backend.
+ await mink.deleteCorpus(corpusId).catch(alertError);
+ // The backend will have updated the remote JWT, so refresh our copy.
+ // The backend uses the corpus list within it when listing available corpora.
+ await refreshJwt();
+ await refreshCorpora();
+ }
+
+ return { deleteCorpus };
+}
diff --git a/src/corpus/exports/CorpusResult.vue b/src/corpus/exports/CorpusResult.vue
index 4d29d2c..e129829 100644
--- a/src/corpus/exports/CorpusResult.vue
+++ b/src/corpus/exports/CorpusResult.vue
@@ -1,6 +1,28 @@
+
+
-
+
{{ $t("exports.help") }}
+
-
-
-
-
diff --git a/src/corpus/exports/Exports.vue b/src/corpus/exports/ExportsPanel.vue
similarity index 88%
rename from src/corpus/exports/Exports.vue
rename to src/corpus/exports/ExportsPanel.vue
index 45a2cc3..57eacdd 100644
--- a/src/corpus/exports/Exports.vue
+++ b/src/corpus/exports/ExportsPanel.vue
@@ -1,3 +1,55 @@
+
+
@@ -24,7 +76,7 @@
name="Strix"
:info="$t('exports.tools.help.strix')"
:can-install="canInstall"
- :is-installed="strixStatus.isDone"
+ :is-installed="jobState?.strix == 'done'"
:show-url="`${strixUrl}?modeSelected=mink&filters=corpus_id:${corpusId}&lang=${locale3}`"
@install="strixInstall()"
/>
@@ -56,56 +108,3 @@