diff --git a/CHANGELOG.md b/CHANGELOG.md index a9f8eb98e..063104cdc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,12 +4,15 @@ ### Added -- TypeScript typings for config/settings -- TypeScript typings for CQP queries +- TypeScript typings for: + - config/settings + - CQP queries + - CorpusListing ### Changed - Replaced Raphael library with Chart.js, used in the pie chart over corpus distribution in statistics +- In the `CorpusListing` class, the methods `getLinked` and `getEnabledByLang` have new parameter signatures ### Refactored diff --git a/app/scripts/backend/kwic-proxy.ts b/app/scripts/backend/kwic-proxy.ts index 35a7d59b5..ecff220ea 100644 --- a/app/scripts/backend/kwic-proxy.ts +++ b/app/scripts/backend/kwic-proxy.ts @@ -41,7 +41,7 @@ export class KwicProxy extends BaseProxy { } function getPageInterval(): Interval { - const hpp: string | number = locationSearchGet("hpp") + const hpp = locationSearchGet("hpp") const itemsPerPage = Number(hpp) || settings.hits_per_page_default const start = (page || 0) * itemsPerPage const end = start + itemsPerPage - 1 diff --git a/app/scripts/components/corpus-updates.ts b/app/scripts/components/corpus-updates.ts index 305dd128d..0ba0c9130 100644 --- a/app/scripts/components/corpus-updates.ts +++ b/app/scripts/components/corpus-updates.ts @@ -3,7 +3,7 @@ import angular, { IScope } from "angular" import moment from "moment" import settings from "@/settings" import { html } from "@/util" -import { LangString } from "@/i18n/types" +import { CorpusTransformed } from "@/settings/config-transformed.types" export default angular.module("korpApp").component("corpusUpdates", { template: html` @@ -67,13 +67,8 @@ export default angular.module("korpApp").component("corpusUpdates", { type CorpusUpdatesScope = IScope & { LIMIT: number - recentUpdates: Corpus[] | null - recentUpdatesFiltered: Corpus[] | null + recentUpdates: CorpusTransformed[] | null + recentUpdatesFiltered: CorpusTransformed[] | null expanded: boolean toggleExpanded: (to?: boolean) => void } - -type Corpus = { - info: { Updated: string } - title: LangString -} diff --git a/app/scripts/corpus_listing.js b/app/scripts/corpus_listing.ts similarity index 73% rename from app/scripts/corpus_listing.js rename to app/scripts/corpus_listing.ts index 295027a0a..e6cef51c0 100644 --- a/app/scripts/corpus_listing.js +++ b/app/scripts/corpus_listing.ts @@ -1,18 +1,39 @@ /** @format */ import _ from "lodash" -import moment from "moment" +import moment, { type Moment } from "moment" import settings from "@/settings" import { locationSearchGet } from "@/util" -import { loc } from "@/i18n" +import { locObj } from "@/i18n" +import { Attribute } from "./settings/config.types" +import { CorpusTransformed } from "./settings/config-transformed.types" +import { LangString } from "./i18n/types" + +export type Filter = { + settings: Attribute + corpora: string[] +} + +export type AttributeOption = { + group: "word" | "word_attr" | "sentence_attr" + value: string + label: LangString +} export class CorpusListing { - constructor(corpora) { + corpora: CorpusTransformed[] + selected: CorpusTransformed[] + struct: Record + structAttributes: Record + commonAttributes: Record + _wordGroup: AttributeOption + + constructor(corpora: Record) { this.struct = corpora this.corpora = _.values(corpora) this.selected = [] } - get(key) { + get(key: string) { return this.struct[key] } @@ -20,11 +41,11 @@ export class CorpusListing { return this.corpora } - map(func) { + map(func: (corpus: CorpusTransformed) => T): T[] { return _.map(this.corpora, func) } - subsetFactory(idArray) { + subsetFactory(idArray: string[]) { // returns a new CorpusListing instance from an id subset. idArray = _.invokeMap(idArray, "toLowerCase") const cl = new CorpusListing(_.pick(this.struct, ...idArray)) @@ -34,41 +55,41 @@ export class CorpusListing { } // only applicable for parallel corpora - getReduceLang() {} + getReduceLang(): string { + return "" + } // Returns an array of all the selected corpora's IDs in uppercase getSelectedCorpora() { return _.map(this.selected, "id") } - select(idArray) { - this.selected = _.values(_.pick.apply(this, [this.struct].concat(idArray))) - + select(idArray: string[]): void { + this.selected = idArray.map((id) => this.struct[id]) this.updateAttributes() } - mapSelectedCorpora(f) { + mapSelectedCorpora(f: (corpus: CorpusTransformed) => T): T[] { return _.map(this.selected, f) } // takes an array of mapping objs and returns their intersection - _mapping_intersection(mappingArray) { - return _.reduce( - mappingArray, - function (a, b) { - const keys_intersect = _.intersection(_.keys(a), _.keys(b)) + _mapping_intersection(mappingArray: T[]): T { + return ( + _.reduce(mappingArray, function (a: T, b: T) { + const keys_intersect = _.intersection(_.keys(a), _.keys(b)) as (keyof T)[] const to_mergea = _.pick(a, ...keys_intersect) const to_mergeb = _.pick(b, ...keys_intersect) return _.merge({}, to_mergea, to_mergeb) - } || {} + }) || ({} as T) ) } - _mapping_union(mappingArray) { - return _.reduce(mappingArray, (a, b) => _.merge(a, b), {}) + _mapping_union(mappingArray: T[]): T { + return _.reduce(mappingArray, (a, b) => _.merge(a, b), {} as T) } - getCurrentAttributes(lang) { + getCurrentAttributes(lang?: string) { // lang not used here, only in parallel mode const attrs = this.mapSelectedCorpora((corpus) => corpus.attributes) return this._invalidateAttrs(attrs) @@ -80,7 +101,7 @@ export class CorpusListing { return this._mapping_intersection(attrs) } - getStructAttrsIntersection() { + getStructAttrsIntersection(lang?: string): Record { const attrs = this.mapSelectedCorpora(function (corpus) { for (let key in corpus["struct_attributes"]) { const value = corpus["struct_attributes"][key] @@ -92,11 +113,11 @@ export class CorpusListing { return this._mapping_intersection(attrs) } - getStructAttrs(lang) { + getStructAttrs(lang?: string): Record { return this.structAttributes } - _getStructAttrs() { + _getStructAttrs(): Record { const attrs = this.mapSelectedCorpora(function (corpus) { for (let key in corpus["struct_attributes"]) { const value = corpus["struct_attributes"][key] @@ -113,7 +134,7 @@ export class CorpusListing { // TODO this code merges datasets from attributes with the same name and // should be moved to the code for extended controller "datasetSelect" - const withDataset = _.filter(_.toPairs(rest), (item) => item[1].dataset) + const withDataset: [string, Attribute][] = _.toPairs(rest).filter((item) => item[1].dataset) $.each(withDataset, function (i, item) { const key = item[0] const val = item[1] @@ -138,8 +159,7 @@ export class CorpusListing { /** Compile list of filters applicable to all selected corpora. */ getDefaultFilters() { - /** @type {Object.} */ - const attrs = {} + const attrs: Record = {} // Collect filters of all selected corpora for (let corpus of this.selected) { @@ -165,7 +185,7 @@ export class CorpusListing { return attrs } - _invalidateAttrs(attrs) { + _invalidateAttrs(attrs: Record[]) { const union = this._mapping_union(attrs) const intersection = this._mapping_intersection(attrs) $.each(union, function (key, value) { @@ -180,7 +200,7 @@ export class CorpusListing { } // returns true if coprus has all attrs, else false - corpusHasAttrs(corpus, attrs) { + corpusHasAttrs(corpus: string, attrs: string[]): boolean { for (let attr of attrs) { if ( attr !== "word" && @@ -192,34 +212,34 @@ export class CorpusListing { return true } - stringifySelected(onlyMain) { + stringifySelected(onlyMain?: boolean): string { return _.map(this.selected, "id") .map((a) => a.toUpperCase()) .join(",") } - stringifyAll() { + stringifyAll(): string { return _.map(this.corpora, "id") .map((a) => a.toUpperCase()) .join(",") } - getWithinKeys() { + getWithinKeys(): string[] { const struct = _.map(this.selected, (corpus) => _.keys(corpus.within)) - return _.union(...(struct || [])) + return _.union(...struct) } - getContextQueryStringFromCorpusId(corpus_ids, prefer, avoid) { + getContextQueryStringFromCorpusId(corpus_ids: string[], prefer: string, avoid: string): string { const corpora = _.map(corpus_ids, (corpus_id) => settings.corpora[corpus_id.toLowerCase()]) return this.getContextQueryStringFromCorpora(_.compact(corpora), prefer, avoid) } - getContextQueryString(prefer, avoid) { + getContextQueryString(prefer: string, avoid: string): string { return this.getContextQueryStringFromCorpora(this.selected, prefer, avoid) } - getContextQueryStringFromCorpora(corpora, prefer, avoid) { - const output = [] + getContextQueryStringFromCorpora(corpora: CorpusTransformed[], prefer: string, avoid: string) { + const output: string[] = [] for (let corpus of corpora) { const contexts = _.keys(corpus.context) if (!contexts.includes(prefer)) { @@ -232,10 +252,10 @@ export class CorpusListing { return _(output).compact().join() } - getWithinParameters() { - const defaultWithin = locationSearchGet("within") || _.keys(settings["default_within"])[0] + getWithinParameters(): { default_within: string; within: string } { + const defaultWithin = locationSearchGet("within") || _.keys(settings.default_within)[0] - const output = [] + const output: string[] = [] for (let corpus of this.selected) { const withins = _.keys(corpus.within) if (!withins.includes(defaultWithin)) { @@ -246,7 +266,7 @@ export class CorpusListing { return { default_within: defaultWithin, within } } - getCommonWithins() { + getCommonWithins(): Record { // only return withins that are available in every selected corpus const allWithins = this.selected.map((corp) => corp.within) const withins = allWithins.reduce( @@ -269,7 +289,7 @@ export class CorpusListing { return withins } - getTimeInterval() { + getTimeInterval(): [number, number] { const all = _(this.selected) .map("time") .filter((item) => item != null) @@ -282,11 +302,8 @@ export class CorpusListing { return [_.first(all), _.last(all)] } - getMomentInterval() { - let from, to - const toUnix = (item) => item.unix() - - const infoGetter = (prop) => { + getMomentInterval(): [Moment, Moment] { + const infoGetter = (prop: "FirstDate" | "LastDate") => { return _(this.selected) .map("info") .map(prop) @@ -298,28 +315,20 @@ export class CorpusListing { const froms = infoGetter("FirstDate") const tos = infoGetter("LastDate") - if (!froms.length) { - from = null - } else { - from = _.minBy(froms, toUnix) - } - if (!tos.length) { - to = null - } else { - to = _.maxBy(tos, toUnix) - } + const from = _.minBy(froms, (item) => item.unix()) || null + const to = _.maxBy(tos, (item) => item.unix()) || null return [from, to] } - getTitleObj(corpus) { + getTitleObj(corpus: string): LangString { return this.struct[corpus].title } /* * Avoid triggering watches etc. by only creating this object once. */ - getWordGroup() { + getWordGroup(): AttributeOption { if (!this._wordGroup) { this._wordGroup = { group: "word", @@ -330,31 +339,27 @@ export class CorpusListing { return this._wordGroup } - getWordAttributeGroups(lang, setOperator) { - let allAttrs - if (setOperator === "union") { - allAttrs = this.getCurrentAttributes(lang) - } else { - allAttrs = this.getCurrentAttributesIntersection() - } + getWordAttributeGroups(lang: string, setOperator: "union" | "intersection"): AttributeOption[] { + const allAttrs = + setOperator === "union" ? this.getCurrentAttributes(lang) : this.getCurrentAttributesIntersection() - const attrs = [] + const attrs: AttributeOption[] = [] for (let key in allAttrs) { const obj = allAttrs[key] if (obj["display_type"] !== "hidden") { - attrs.push(_.extend({ group: "word_attr", value: key }, obj)) + attrs.push({ group: "word_attr", value: key, ...obj }) } } return attrs } - getWordAttribute(attribute, lang) { + getWordAttribute(attribute: string, lang: string): Attribute { const attributes = this.getCurrentAttributes(lang) return attributes[attribute] } - getStructAttributeGroups(lang, setOperator) { + getStructAttributeGroups(lang: string, setOperator: "union" | "intersection"): AttributeOption[] { let allAttrs if (setOperator === "union") { allAttrs = this.getStructAttrs(lang) @@ -364,28 +369,28 @@ export class CorpusListing { const common = this.commonAttributes - let sentAttrs = [] + let sentAttrs: AttributeOption[] = [] const object = _.extend({}, common, allAttrs) for (let key in object) { const obj = object[key] if (obj["display_type"] !== "hidden") { - sentAttrs.push(_.extend({ group: "sentence_attr", value: key }, obj)) + sentAttrs.push({ group: "sentence_attr", value: key, ...obj }) } } - sentAttrs = _.sortBy(sentAttrs, (item) => loc(item.label)) + sentAttrs = _.sortBy(sentAttrs, (item) => locObj(item.label)) return sentAttrs } - getAttributeGroups(lang) { + getAttributeGroups(lang: string): AttributeOption[] { const word = this.getWordGroup() const attrs = this.getWordAttributeGroups(lang, "union") const sentAttrs = this.getStructAttributeGroups(lang, "union") return [word].concat(attrs, sentAttrs) } - getStatsAttributeGroups(lang) { + getStatsAttributeGroups(lang: string): AttributeOption[] { const word = this.getWordGroup() const wordOp = settings["reduce_word_attribute_selector"] || "union" @@ -400,13 +405,13 @@ export class CorpusListing { // update attributes so that we don't need to check them multiple times // currently done only for common and struct attributes, but code for // positional could be added here, but is tricky because parallel mode lang might be needed - updateAttributes() { + updateAttributes(): void { const common_keys = _.compact(_.flatten(_.map(this.selected, (corp) => _.keys(corp.common_attributes)))) this.commonAttributes = _.pick(settings["common_struct_types"], ...common_keys) this.structAttributes = this._getStructAttrs() } - isDateInterval(type) { + isDateInterval(type: string): boolean { if (_.isEmpty(type)) { return false } diff --git a/app/scripts/data_init.js b/app/scripts/data_init.js index dccef4423..bae0d2271 100644 --- a/app/scripts/data_init.js +++ b/app/scripts/data_init.js @@ -128,8 +128,8 @@ async function getConfig() { function transformConfig(modeSettings) { // only if the current mode is parallel, we load the special code required if (modeSettings.parallel) { - require("./parallel/corpus_listing.js") - require("./parallel/stats_proxy.ts") + require("./parallel/corpus_listing") + require("./parallel/stats_proxy") } function rename(obj, from, to) { diff --git a/app/scripts/parallel/corpus_listing.js b/app/scripts/parallel/corpus_listing.ts similarity index 59% rename from app/scripts/parallel/corpus_listing.js rename to app/scripts/parallel/corpus_listing.ts index 9c9465f4a..2827203c2 100644 --- a/app/scripts/parallel/corpus_listing.js +++ b/app/scripts/parallel/corpus_listing.ts @@ -3,63 +3,59 @@ import _ from "lodash" import settings from "@/settings" import { CorpusListing } from "@/corpus_listing" import { locationSearchGet } from "@/util" +import { CorpusTransformed } from "@/settings/config-transformed.types" +import { Attribute } from "@/settings/config.types" +import { LangString } from "@/i18n/types" export class ParallelCorpusListing extends CorpusListing { - constructor(corpora) { + activeLangs: string[] + + constructor(corpora: Record) { super(corpora) - const hash = window.location.hash.substr(2) - for (let item of hash.split("&")) { - var parts = item.split("=") - if (parts[0] == "parallel_corpora") { - this.setActiveLangs(parts[1].split(",")) - break - } - } + // Cannot use Angular helpers (`locationSearchGet`) here, it's not initialized yet. + const activeLangs = new URLSearchParams(window.location.hash.slice(2)).get("parallel_corpora") + this.setActiveLangs(activeLangs.split(",")) } - select(idArray) { - this.selected = [] - $.each(idArray, (i, id) => { - const corp = this.struct[id] - this.selected = this.selected.concat(this.getLinked(corp, true, false)) - }) - - this.selected = _.uniq(this.selected) + select(idArray: string[]): void { + this.selected = _.uniq(idArray.flatMap((id) => this.getLinked(this.struct[id]))) this.updateAttributes() } - setActiveLangs(langlist) { + setActiveLangs(langlist: string[]): void { this.activeLangs = langlist } - getReduceLang() { + getReduceLang(): string { return this.activeLangs[0] } - getCurrentAttributes(lang) { + getCurrentAttributes(lang?: string): Record { if (_.isEmpty(lang)) { lang = settings.corpusListing.getReduceLang() } const corpora = _.filter(this.selected, (item) => item.lang === lang) - const struct = _.reduce(corpora, (a, b) => $.extend({}, a.attributes, b.attributes), {}) - return struct + return corpora.reduce((attrs, corpus) => ({ ...attrs, ...corpus.attributes }), {} as Record) } - getStructAttrs(lang) { + getStructAttrs(lang?: string): Record { if (_.isEmpty(lang)) { lang = settings.corpusListing.getReduceLang() } const corpora = _.filter(this.selected, (item) => item.lang === lang) - const struct = _.reduce(corpora, (a, b) => $.extend({}, a["struct_attributes"], b["struct_attributes"]), {}) - $.each(struct, (key, val) => (val["is_struct_attr"] = true)) + const struct = corpora.reduce( + (attrs, corpus) => ({ ...attrs, ...corpus.struct_attributes }), + {} as Record + ) + Object.values(struct).forEach((attr) => (attr.is_struct_attr = true)) return struct } - getStructAttrsIntersection(lang) { + getStructAttrsIntersection(lang: string): Record { const corpora = _.filter(this.selected, (item) => item.lang === lang) const attrs = _.map(corpora, function (corpus) { for (let key in corpus["struct_attributes"]) { @@ -72,45 +68,20 @@ export class ParallelCorpusListing extends CorpusListing { return this._mapping_intersection(attrs) } - getLinked(corp, andSelf, only_selected) { - if (andSelf == null) { - andSelf = false - } - if (only_selected == null) { - only_selected = true - } - const target = only_selected ? this.selected : this.struct - let output = _.filter(target, (item) => (corp["linked_to"] || []).includes(item.id)) - if (andSelf) { - output = [corp].concat(output) - } - return output + getLinked(corp: CorpusTransformed, only_selected?: boolean) { + const target = only_selected ? this.selected : Object.values(this.struct) + let output: CorpusTransformed[] = target.filter((item) => (corp["linked_to"] || []).includes(item.id)) + return [corp].concat(output) } - getEnabledByLang(lang, andSelf, flatten) { - if (andSelf == null) { - andSelf = false - } - if (flatten == null) { - flatten = true - } + getEnabledByLang(lang: string): CorpusTransformed[][] { const corps = _.filter(this.selected, (item) => item["lang"] === lang) - const output = _(corps) - .map((item) => { - return this.getLinked(item, andSelf) - }) - .value() - - if (flatten) { - return _.flatten(output) - } else { - return output - } + return corps.map((item) => this.getLinked(item, true)) } - getLinksFromLangs(activeLangs) { + getLinksFromLangs(activeLangs: string[]): CorpusTransformed[][] { if (activeLangs.length === 1) { - return this.getEnabledByLang(activeLangs[0], true, false) + return this.getEnabledByLang(activeLangs[0]) } // get the languages that are enabled given a list of active languages const main = _.filter(this.selected, (corp) => corp.lang === activeLangs[0]) @@ -131,7 +102,7 @@ export class ParallelCorpusListing extends CorpusListing { return output } - getAttributeQuery(attr) { + getAttributeQuery(attr: "context" | "within"): string { // gets the within and context queries const struct = this.getLinksFromLangs(this.activeLangs) @@ -143,12 +114,7 @@ export class ParallelCorpusListing extends CorpusListing { const other = corps.slice(1) const pair = _.map(other, function (corp) { - let a - if (mainIsPivot) { - a = _.keys(corp[attr])[0] - } else { - a = _.keys(corps[0][attr])[0] - } + const a = mainIsPivot ? _.keys(corp[attr])[0] : _.keys(corps[0][attr])[0] return mainId + "|" + corp.id.toUpperCase() + ":" + a }) return output.push(pair) @@ -157,17 +123,17 @@ export class ParallelCorpusListing extends CorpusListing { return output.join(",") } - getContextQueryString() { + getContextQueryString(): string { return this.getAttributeQuery("context") } - getWithinParameters() { + getWithinParameters(): { default_within: string; within: string } { const defaultWithin = locationSearchGet("within") || _.keys(settings["default_within"])[0] const within = this.getAttributeQuery("within") return { default_within: defaultWithin, within } } - stringifySelected(onlyMain) { + stringifySelected(onlyMain?: boolean): string { let struct = this.getLinksFromLangs(this.activeLangs) if (onlyMain) { struct = _.map(struct, (pair) => { @@ -182,7 +148,6 @@ export class ParallelCorpusListing extends CorpusListing { } const output = [] - // $.each(struct, function(i, item) { for (let i = 0; i < struct.length; i++) { const item = struct[i] var main = item[0] @@ -194,11 +159,11 @@ export class ParallelCorpusListing extends CorpusListing { return output.join(",") } - get(corpusID) { + get(corpusID: string): CorpusTransformed { return this.struct[corpusID.split("|")[1]] } - getTitleObj(corpusID) { + getTitleObj(corpusID: string): LangString { return this.get(corpusID).title } } diff --git a/app/scripts/settings/config-transformed.types.ts b/app/scripts/settings/config-transformed.types.ts index 7c3049a52..9a2b45b5f 100644 --- a/app/scripts/settings/config-transformed.types.ts +++ b/app/scripts/settings/config-transformed.types.ts @@ -35,4 +35,7 @@ export type CorpusTransformed = Omit< LastDate?: string Protected?: boolean } + common_attributes?: Record + time?: Record + non_time?: number } diff --git a/app/scripts/settings/config.types.ts b/app/scripts/settings/config.types.ts index eaf70a8e5..283202342 100644 --- a/app/scripts/settings/config.types.ts +++ b/app/scripts/settings/config.types.ts @@ -29,6 +29,8 @@ export type Corpus = { context: Labeled[] description: LangString id: string + lang?: string + pivot?: boolean pos_attributes: string[] struct_attributes: string[] custom_attributes: string[] diff --git a/app/scripts/timeseries.ts b/app/scripts/timeseries.ts index 82017d9bd..07196eb91 100644 --- a/app/scripts/timeseries.ts +++ b/app/scripts/timeseries.ts @@ -32,16 +32,14 @@ export const getSeries = () => fromPairs(getTimeDataPairs()) as YearSeries /** Get data size per year of selected corpora. */ export function getSeriesSelected() { - const corpora: { time?: YearSeries }[] = settings.corpusListing.selected // `pickBy` removes zeroes. - const series = corpora.map((corpus) => ("time" in corpus ? pickBy(corpus.time) : {})) + const series = settings.corpusListing.selected.map((corpus) => ("time" in corpus ? pickBy(corpus.time) : {})) return sumYearSeries(...series) } /** Get data size of unknown year in selected corpora */ export function getCountUndatedSelected() { - const corpora: { non_time?: number }[] = settings.corpusListing.selected - return corpora.reduce((sum, corpus) => sum + (corpus["non_time"] || 0), 0) + return settings.corpusListing.selected.reduce((sum, corpus) => sum + (corpus["non_time"] || 0), 0) } /** Get first and last year in all available corpora. */ diff --git a/app/scripts/util.ts b/app/scripts/util.ts index 13d349a36..956f47497 100644 --- a/app/scripts/util.ts +++ b/app/scripts/util.ts @@ -40,7 +40,8 @@ export const withService = (name: K, fn: (servi * Get values from the URL search string via Angular. * Only use this in code outside Angular. Inside, use `$location.search()`. */ -export const locationSearchGet = (key: string) => withService("$location", ($location) => $location.search()[key]) +export const locationSearchGet = (key: string): string => + withService("$location", ($location) => $location.search()[key]) /** * Set values in the URL search string via Angular.