From ac28a430f212d8c7fc5b69543fb1f28303d875e2 Mon Sep 17 00:00:00 2001 From: Max Varlamov Date: Wed, 7 Sep 2022 18:27:23 +0300 Subject: [PATCH 01/16] TALCR-228. Concept selector draft --- package.json | 1 + src/_locales/en/messages.json | 1 + src/_locales/ru/messages.json | 1 + src/background/background.js | 8 + src/devtools/views/SelectorEdit.html | 4 + src/scripts/Controller.js | 135 +- src/scripts/Selector/SelectorConcept.js | 7 + src/scripts/SelectorList.js | 3 + src/scripts/StoreTalismanApi.js | 32 + src/scripts/TalismanStoreDevtools.js | 8 + yarn.lock | 15968 +++++++++++----------- 11 files changed, 8248 insertions(+), 7920 deletions(-) create mode 100644 src/scripts/Selector/SelectorConcept.js diff --git a/package.json b/package.json index ff72b15..477fae9 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "renderjson": "^1.4.0", "spark-md5": "^3.0.1", "sugar": "^1.5.0", + "transliteration": "^2.3.5", "url-join": "^5.0.0", "webextension-polyfill": "^0.7.0" }, diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json index f8ab078..9596861 100644 --- a/src/_locales/en/messages.json +++ b/src/_locales/en/messages.json @@ -150,6 +150,7 @@ "SelectorElementClick": { "message": "Element click" }, "SelectorGroup": { "message": "Grouped" }, "SelectorPageURL": { "message": "Page URL" }, + "SelectorConcept": { "message": "Concept" }, "sitemap_scrape_config_requestInterval": { "message": "Request interval" }, "sitemap_scrape_config_requestIntervalRandomness": { "message": "Request interval randomness" }, diff --git a/src/_locales/ru/messages.json b/src/_locales/ru/messages.json index 0944d87..241ef20 100644 --- a/src/_locales/ru/messages.json +++ b/src/_locales/ru/messages.json @@ -158,6 +158,7 @@ "ConstantValue": { "message": "Константа" }, "SelectorInputValue": { "message": "Вставка значения" }, "SelectorPageURL": { "message": "URL страницы" }, + "SelectorConcept": { "message": "Селектор концепта" }, "sitemap_scrape_requestInterval": { "message": "Интервал между запросами" }, "sitemap_scrape_requestIntervalRandomness": { "message": "Случайность между запросами" }, diff --git a/src/background/background.js b/src/background/background.js index 7d6c7d6..4e41caa 100644 --- a/src/background/background.js +++ b/src/background/background.js @@ -96,6 +96,14 @@ browser.runtime.onMessage.addListener(async request => { return store.getSitemapData(Sitemap.sitemapFromObj(request.sitemap)); } + if (request.listConceptTypes) { + return store.listConceptTypes(); + } + + if (request.getConceptType) { + return store.getConceptType(request.conceptTypeId); + } + if (request.scrapeSitemap) { const sitemap = Sitemap.sitemapFromObj(request.sitemap); const queue = new Queue(); diff --git a/src/devtools/views/SelectorEdit.html b/src/devtools/views/SelectorEdit.html index f7757ab..741ae2f 100644 --- a/src/devtools/views/SelectorEdit.html +++ b/src/devtools/views/SelectorEdit.html @@ -36,6 +36,10 @@ +
+ +
+
+
+ + +
+
+ +
+
+
+
diff --git a/src/scripts/Controller.js b/src/scripts/Controller.js index 5dd89a1..b57c650 100644 --- a/src/scripts/Controller.js +++ b/src/scripts/Controller.js @@ -1437,6 +1437,7 @@ export default class SitemapController { const delay = $('#edit-selector [name=delay]').val(); const outerHTML = $('#edit-selector [id=outerHTML]').is(':checked'); const mergeIntoList = $('#edit-selector [name=mergeIntoList]').is(':checked'); + const dontFlatten = $('#edit-selector [name=dontFlatten]').is(':checked'); const extractAttribute = $('#edit-selector [name=extractAttribute]').val(); const extractStyle = $('#edit-selector [name=extractStyle]').val(); const value = $('#edit-selector [name=value]').val(); @@ -1499,6 +1500,7 @@ export default class SitemapController { textmanipulation, stringReplacement, mergeIntoList, + dontFlatten, outerHTML, uuid, conceptTypeId, diff --git a/src/scripts/DataExtractor.js b/src/scripts/DataExtractor.js index 39a47a7..fba1a3d 100644 --- a/src/scripts/DataExtractor.js +++ b/src/scripts/DataExtractor.js @@ -170,7 +170,11 @@ export default class DataExtractor { newParentElement ); deferredChildCommonData.done(function (data) { - d.resolve(data); + if (selector.dontFlatten) { + d.resolve({ [selector.id]: data }); + } else { + d.resolve(data); + } }); } } else { diff --git a/src/scripts/Job.js b/src/scripts/Job.js index 36e6008..f8a0e87 100644 --- a/src/scripts/Job.js +++ b/src/scripts/Job.js @@ -1,5 +1,5 @@ export default class Job { - constructor(url, parentSelector, scraper, parentJob, baseData) { + constructor(url, parentSelector, scraper, parentJob, baseData, baseDataPath) { if (parentJob !== undefined) { this.url = this.combineUrls(parentJob.url, url); } else { @@ -9,6 +9,7 @@ export default class Job { this.scraper = scraper; this.dataItems = []; this.baseData = baseData || {}; + this.baseDataPath = baseDataPath || []; } combineUrls(parentUrl, childUrl) { @@ -69,17 +70,12 @@ export default class Job { this.url, sitemap, this.parentSelector, - function (results) { + results => { // merge data with data from initialization - for (const i in results) { - const result = results[i]; - for (const key in this.baseData) { - if (!(key in result)) { - result[key] = this.baseData[key]; - } - } - this.dataItems.push(result); - } + results.forEach(result => { + const mergedResult = this.mergeWithBaseData(result); + this.dataItems.push(mergedResult); + }); if (sitemap) { // table selector can dynamically add columns (addMissingColumns Feature) @@ -88,11 +84,20 @@ export default class Job { console.log(job); callback(job); - }.bind(this), + }, this ); } + mergeWithBaseData(result) { + const mergedData = structuredClone(this.baseData); + const { _url, _timestamp, ...resultData } = result; + const insertAt = this.baseDataPath.reduce((data, key) => data[key] || {}, mergedData); + Object.assign(mergedData, { _url, _timestamp }); + Object.assign(insertAt, resultData); + return mergedData; + } + getResults() { return this.dataItems; } diff --git a/src/scripts/Scraper.js b/src/scripts/Scraper.js index 6898918..112e40b 100644 --- a/src/scripts/Scraper.js +++ b/src/scripts/Scraper.js @@ -127,38 +127,32 @@ export default class Scraper { const deferredDatamanipulations = []; const records = job.getResults(); - records.forEach( - function (record) { - // var record = JSON.parse(JSON.stringify(rec)); - - deferredDatamanipulations.push(this.saveFile.bind(this, record)); - - // @TODO refactor job exstraction to a seperate method - if (this.recordCanHaveChildJobs(record)) { - // var followSelectorId = record._followSelectorId; - const followURL = record._follow; - const followSelectorId = record._followSelectorId; - delete record._follow; - delete record._followSelectorId; - const newJob = new Job(followURL, followSelectorId, this, job, record); - if (this.queue.canBeAdded(newJob)) { - this.queue.add(newJob); - } - // store already scraped links - else { - console.log('Ignoring next'); - console.log(record); - // scrapedRecords.push(record); - } + records.forEach(record => { + deferredDatamanipulations.push(this.saveFile.bind(this, record)); + + const followEntries = Array.from(this.extractFollowEntries(record)); + + if (!followEntries.length) { + scrapedRecords.push(record); + return; + } + + followEntries.forEach(({ followURL, followSelectorId, recordPath }) => { + const newJob = new Job( + followURL, + followSelectorId, + this, + job, + record, + recordPath + ); + if (this.queue.canBeAdded(newJob)) { + this.queue.add(newJob); } else { - if (record._follow !== undefined) { - delete record._follow; - delete record._followSelectorId; - } - scrapedRecords.push(record); + console.log('Ignoring job', newJob); } - }.bind(this) - ); + }); + }); $.whenCallSequentially(deferredDatamanipulations).done( function () { @@ -187,4 +181,27 @@ export default class Scraper { }.bind(this) ); } + + *extractFollowEntries(record, path = []) { + if (typeof record !== 'object') { + return; + } + + const { _follow, _followSelectorId } = record; + if (_follow !== undefined) { + delete record._follow; + delete record._followSelectorId; + if (this.recordCanHaveChildJobs({ _follow, _followSelectorId })) { + yield { + followURL: _follow, + followSelectorId: _followSelectorId, + recordPath: path, + }; + } + } + + for (const key of Object.keys(record)) { + yield* this.extractFollowEntries(record[key], [...path, key]); + } + } } diff --git a/src/scripts/Selector/SelectorElement.js b/src/scripts/Selector/SelectorElement.js index af00fa7..ca95e1f 100644 --- a/src/scripts/Selector/SelectorElement.js +++ b/src/scripts/Selector/SelectorElement.js @@ -35,6 +35,6 @@ export default class SelectorElement extends Selector { } getFeatures() { - return ['selector', 'multiple', 'delay', 'mergeIntoList']; + return ['selector', 'multiple', 'delay', 'mergeIntoList', 'dontFlatten']; } } diff --git a/src/scripts/Selector/SelectorElementClick.js b/src/scripts/Selector/SelectorElementClick.js index d89b239..0fe05c0 100644 --- a/src/scripts/Selector/SelectorElementClick.js +++ b/src/scripts/Selector/SelectorElementClick.js @@ -144,6 +144,7 @@ export default class SelectorElementClick extends Selector { 'clickElementUniquenessType', 'paginationLimit', 'mergeIntoList', + 'dontFlatten', ]; } } diff --git a/src/scripts/Selector/SelectorElementScroll.js b/src/scripts/Selector/SelectorElementScroll.js index df371ed..2658f06 100644 --- a/src/scripts/Selector/SelectorElementScroll.js +++ b/src/scripts/Selector/SelectorElementScroll.js @@ -101,6 +101,7 @@ export default class SelectorElementScroll extends Selector { 'delay', 'paginationLimit', 'mergeIntoList', + 'dontFlatten', ]; } } From f4dc86425b5d76af4f873ce4aa3a5b7c1469754f Mon Sep 17 00:00:00 2001 From: Max Varlamov Date: Mon, 12 Sep 2022 17:58:12 +0300 Subject: [PATCH 04/16] TALCR-228. Moved kb related code out of controller, added support for directed links --- src/_locales/en/messages.json | 8 +- src/_locales/ru/messages.json | 8 +- src/scripts/Controller.js | 122 +++-------------- src/scripts/StoreTalismanApi.js | 7 + src/scripts/TalismanKB.js | 223 ++++++++++++++++++++++++++++++++ 5 files changed, 260 insertions(+), 108 deletions(-) create mode 100644 src/scripts/TalismanKB.js diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json index 9363a7a..79fd967 100644 --- a/src/_locales/en/messages.json +++ b/src/_locales/en/messages.json @@ -342,5 +342,11 @@ }, "popup_ws_version": { "message": "Web Scraper version: " - } + }, + + "link_types_for_concept_type": { "message": "Link types for concept type" }, + "concept_types_for_link_type": { "message": "Concept types for link type" }, + "prop_types_for_concept_type": { "message": "Property types for concept type" }, + "prop_types_for_link_type": { "message": "Property types for link type" }, + "all_concept_types": { "message": "All concept types" } } diff --git a/src/_locales/ru/messages.json b/src/_locales/ru/messages.json index f9cff9e..167c93c 100644 --- a/src/_locales/ru/messages.json +++ b/src/_locales/ru/messages.json @@ -378,5 +378,11 @@ }, "popup_ws_version": { "message": "Версия Web Scraper: " - } + }, + + "link_types_for_concept_type": { "message": "Типы связей для типа концепта" }, + "concept_types_for_link_type": { "message": "Типы концептов для типа связи" }, + "prop_types_for_concept_type": { "message": "Типы характеристик для типа концепта" }, + "prop_types_for_link_type": { "message": "Типы характеристик для типа связи" }, + "all_concept_types": { "message": "Все типы концептов" } } diff --git a/src/scripts/Controller.js b/src/scripts/Controller.js index b57c650..b57ef73 100644 --- a/src/scripts/Controller.js +++ b/src/scripts/Controller.js @@ -17,6 +17,7 @@ import SelectorTable from './Selector/SelectorTable'; import Model from './Model'; import Translator from './Translator'; import SelectorElement from './Selector/SelectorElement'; +import TalismanKB from './TalismanKB'; export default class SitemapController { constructor(store, templateDir) { @@ -72,9 +73,6 @@ export default class SitemapController { { type: 'SelectorGroup', }, - { - type: 'SelectorConcept', // TODO only for tlsmn storage - }, ]; this.selectorTypes = this.selectorTypes.map(typeObj => { return { ...typeObj, title: Translator.getTranslationByKey(typeObj.type) }; @@ -94,6 +92,11 @@ export default class SitemapController { return value; }) .set_sort_objects(true); + + if (store.storageType === 'StoreTalismanApi') { + this.kb = new TalismanKB(store); + } + return this.init(); } @@ -1272,75 +1275,16 @@ export default class SitemapController { minLength: 0, }; - const parentTalismanTypeIds = this.getParentTalismanTypeIds(selector); - const conceptTypeIds = parentTalismanTypeIds.filter(({ type }) => type === 'concept'); - const conceptTypes = await this.store.getConceptTypes(conceptTypeIds.map(({ id }) => id)); - const linkTypeIds = parentTalismanTypeIds.filter(({ type }) => type === 'link'); - const linkTypes = await this.store.getLinkTypes(linkTypeIds.map(({ id }) => id)); - - const hints = []; - - if (selector.willReturnElements()) { - // may be concept type or link type - - conceptTypes.forEach(conceptType => { - hints.push( - ...conceptType.listConceptLinkType.map(({ id, name }) => ({ - entity: `Связи типа концепта ${conceptType.name}`, - field: `${id} ${name}`, - fieldName: this.makeTalismanSelectorId('link', id, name), - })) - ); - }); - - linkTypes.forEach(linkType => { - hints.push( - ...[linkType.conceptToType, linkType.conceptFromType].map(({ id, name }) => ({ - entity: `Концепты типа связи ${linkType.name}`, - field: `${id} ${name}`, - fieldName: this.makeTalismanSelectorId('concept', id, name), - })) - ); - }); - - // show hints with all concept types - const allConceptTypes = await this.store.listAllConceptTypes(); - hints.push( - ...allConceptTypes.map(({ id, name }) => ({ - entity: 'Все типы концептов', // TODO translate - field: `${id} ${name}`, - fieldName: this.makeTalismanSelectorId('concept', id, name), - })) - ); - } else if (parentTalismanTypeIds.length) { - // concept or link property - - conceptTypes.forEach(conceptType => { - hints.push( - ...conceptType.listConceptPropertyType.map(({ id, name }) => ({ - entity: `Характеристики типа концепта ${conceptType.name}`, - field: `${id} ${name}`, - fieldName: this.makeTalismanSelectorId('property', id, name), - })) - ); - }); - - linkTypes.forEach(linkType => { - hints.push( - ...linkType.listConceptLinkPropertyType.map(({ id, name }) => ({ - entity: `Характеристики типа связи ${linkType.name}`, - field: `${id} ${name}`, - fieldName: this.makeTalismanSelectorId('property', id, name), - })) - ); - }); - } else { - hints.push(...this.state.currentSitemap.model, { - entity: '', - field: '', - fieldName: selector.id, - }); - } + const hints = this.kb + ? await this.kb.generateIdHints(selector, this.state.currentSitemap) + : [ + ...this.state.currentSitemap.model, + { + entity: '', + field: '', + fieldName: selector.id, + }, + ]; $selectorIdInput.flexdatalist({ ...idDatalistOptions, @@ -1348,40 +1292,6 @@ export default class SitemapController { }); } - makeTalismanSelectorId(type, id, name) { - return `[tlsmn:${type}:${id}] ${name}`; - } - - getTalismanTypeId(selector) { - const match = /^\[tlsmn:([^:]+):([^:]+)]/.exec(selector.id); - if (match) { - return { type: match[1], id: match[2] }; - } - return undefined; - } - - getParentTalismanTypeIds(selector) { - const sitemap = this.state.currentSitemap; - const seenSelectors = new Set([sitemap.rootSelector.uuid, selector.uuid]); - const selectorQueue = selector.parentSelectors.filter(uid => !seenSelectors.has(uid)); - const parentTypeIds = []; - while (selectorQueue.length) { - const parentSelector = sitemap.getSelectorByUid(selectorQueue.pop()); - const typeId = this.getTalismanTypeId(parentSelector); - if (typeId && typeId.type !== 'property') { - parentTypeIds.push({ ...typeId, selectorUid: parentSelector.uuid }); - } else { - parentSelector.parentSelectors.forEach(uid => { - if (!seenSelectors.has(uid)) { - seenSelectors.add(uid); - selectorQueue.push(uid); - } - }); - } - } - return parentTypeIds; - } - async saveSelector(button) { const sitemap = this.state.currentSitemap; const selector = this.state.currentSelector; diff --git a/src/scripts/StoreTalismanApi.js b/src/scripts/StoreTalismanApi.js index d60d42d..d0a977f 100644 --- a/src/scripts/StoreTalismanApi.js +++ b/src/scripts/StoreTalismanApi.js @@ -98,6 +98,13 @@ export default class StoreTalismanApi extends StoreRestApi { listConceptLinkType { id name + isDirected + conceptFromType { + id + } + conceptToType { + id + } } } }`, diff --git a/src/scripts/TalismanKB.js b/src/scripts/TalismanKB.js new file mode 100644 index 0000000..b84ce82 --- /dev/null +++ b/src/scripts/TalismanKB.js @@ -0,0 +1,223 @@ +import Translator from './Translator'; +import 'sugar'; + +const KB_TYPES = { + CONCEPT: 'c', + LINK: 'l', + CONCEPT_PROPERTY: 'cp', + LINK_PROPERTY: 'lp', +}; + +const LINK_DIRECTION = { + RIGHT: '->', + LEFT: '<-', + NONE: '-', +}; + +/** + * Utilities for integration with Talisman knowledge base + */ +export default class TalismanKB { + constructor(store) { + this.store = store; + } + + makeSelectorId(kbType) { + const { type, id, parentId, direction, childId, name } = kbType; + if (type === KB_TYPES.CONCEPT) { + return `[tlsmn:${type}:${id}] ${name}`; + } + if (type === KB_TYPES.LINK) { + return `[tlsmn:${type}:${id}:${parentId}:${direction}:${childId}] ${name}`; + } + return `[tlsmn:${type}:${id}:${parentId}] ${name}`; + } + + getKBType(selector) { + const match = /^\[tlsmn:(.+?)]/.exec(selector.id); + if (!match) { + return undefined; + } + const [type, id, parentId, direction, childId] = match[1].split(':'); + if (type === KB_TYPES.CONCEPT) { + return { type, id }; + } + if (type === KB_TYPES.LINK) { + return { type, id, parentId, direction, childId }; + } + if (type in KB_TYPES) { + return { type, id, parentId }; + } + return undefined; + } + + getParentKBTypes(selector, sitemap) { + const seenSelectors = new Set([sitemap.rootSelector.uuid, selector.uuid]); + const selectorQueue = selector.parentSelectors.filter(uid => !seenSelectors.has(uid)); + const parentKBTypes = []; + while (selectorQueue.length) { + const parentSelector = sitemap.getSelectorByUid(selectorQueue.pop()); + const kbType = this.getKBType(parentSelector); + if (kbType && (kbType.type === KB_TYPES.CONCEPT || kbType.type === KB_TYPES.LINK)) { + parentKBTypes.push(kbType); + } else { + parentSelector.parentSelectors.forEach(uid => { + if (!seenSelectors.has(uid)) { + seenSelectors.add(uid); + selectorQueue.push(uid); + } + }); + } + } + return parentKBTypes; + } + + makeHintField(kbType) { + const field = `#${kbType.id} ${kbType.name}`; + if (kbType.type === KB_TYPES.LINK && kbType.direction !== LINK_DIRECTION.NONE) { + return `${field} (${kbType.direction})`; + } + return field; + } + + makeSelectorIdHint(entity, kbType) { + return { + entity, + field: this.makeHintField(kbType), + fieldName: this.makeSelectorId(kbType), + }; + } + + async getPropertyTypeHintsForConceptType(type) { + const conceptType = await this.store.getSingleConceptType(type.id); + const entityPrefix = Translator.getTranslationByKey('prop_types_for_concept_type'); + const entity = `${entityPrefix} ${this.makeHintField({ ...type, ...conceptType })}`; + return conceptType.listConceptPropertyType.map(propertyType => + this.makeSelectorIdHint(entity, { + type: KB_TYPES.CONCEPT_PROPERTY, + id: propertyType.id, + name: propertyType.name, + parentId: conceptType.id, + }) + ); + } + + async getLinkTypeHintsForConceptType(type) { + const conceptType = await this.store.getSingleConceptType(type.id); + const entityPrefix = Translator.getTranslationByKey('link_types_for_concept_type'); + const entity = `${entityPrefix} ${this.makeHintField({ ...type, ...conceptType })}`; + return conceptType.listConceptLinkType.flatMap(linkType => { + const parentId = conceptType.id; + const childId = + parentId === linkType.conceptFromType.id + ? linkType.conceptToType.id + : linkType.conceptFromType.id; + const directions = []; + if (!linkType.isDirected) { + directions.push(LINK_DIRECTION.NONE); + } else { + if (parentId === linkType.conceptFromType.id) { + directions.push(LINK_DIRECTION.RIGHT); + } + if (parentId === linkType.conceptToType.id) { + directions.push(LINK_DIRECTION.LEFT); + } + } + return directions.map(direction => + this.makeSelectorIdHint(entity, { + type: KB_TYPES.LINK, + id: linkType.id, + name: linkType.name, + parentId, + childId, + direction, + }) + ); + }); + } + + async getChildConceptTypeHintForLinkType(type) { + const linkType = await this.store.getSingleLinkType(type.id); + const entityPrefix = Translator.getTranslationByKey('concept_types_for_link_type'); + const entity = `${entityPrefix} ${this.makeHintField({ ...type, ...linkType })}`; + const conceptType = + type.parentId === linkType.conceptFromType.id + ? linkType.conceptToType + : linkType.conceptFromType; + return this.makeSelectorIdHint(entity, { + type: KB_TYPES.CONCEPT, + id: conceptType.id, + name: conceptType.name, + }); + } + + async getPropertyTypeHintsForLinkType(type) { + const linkType = await this.store.getSingleLinkType(type.id); + const entityPrefix = Translator.getTranslationByKey('prop_types_for_link_type'); + const entity = `${entityPrefix} ${this.makeHintField({ ...type, ...linkType })}`; + return linkType.listConceptLinkPropertyType.map(propertyType => + this.makeSelectorIdHint(entity, { + type: KB_TYPES.LINK_PROPERTY, + id: propertyType.id, + name: propertyType.name, + parentId: linkType.id, + }) + ); + } + + async getAllConceptTypeHints() { + const entity = Translator.getTranslationByKey('all_concept_types'); + const conceptTypes = await this.store.listAllConceptTypes(); + return conceptTypes.map(({ id, name }) => + this.makeSelectorIdHint(entity, { type: KB_TYPES.CONCEPT, id, name }) + ); + } + + async generateIdHints(selector, sitemap) { + const parentKBTypes = this.getParentKBTypes(selector, sitemap); + + const parentConceptTypes = parentKBTypes + .filter(({ type }) => type === KB_TYPES.CONCEPT) + .unique('id'); + const parentLinkTypes = parentKBTypes + .filter(({ type }) => type === KB_TYPES.LINK) + .unique('id'); + + const hintsPromises = []; + + if (selector.willReturnElements()) { + // may be concept type or link type + + hintsPromises.push( + ...parentConceptTypes.map(conceptType => + this.getLinkTypeHintsForConceptType(conceptType) + ) + ); + + hintsPromises.push( + ...parentLinkTypes.map(linkType => + this.getChildConceptTypeHintForLinkType(linkType) + ) + ); + + // show hints with all concept types + hintsPromises.push(this.getAllConceptTypeHints()); + } else if (!selector.canCreateNewJobs()) { + // concept or link property + + hintsPromises.push( + ...parentConceptTypes.map(conceptType => + this.getPropertyTypeHintsForConceptType(conceptType) + ) + ); + + hintsPromises.push( + ...parentLinkTypes.map(linkType => this.getPropertyTypeHintsForLinkType(linkType)) + ); + } + + // TODO improve error handling + const hints = await Promise.all(hintsPromises.map(promise => promise.catch(console.error))); + return hints.compact().flatten(); + } +} From 5233f11220767d2b49f48b78e55b5bed21132c46 Mon Sep 17 00:00:00 2001 From: Max Varlamov Date: Mon, 12 Sep 2022 18:00:22 +0300 Subject: [PATCH 05/16] TALCR-228. Remove concept selector --- src/devtools/views/SelectorEdit.html | 5 +---- src/scripts/Selector/SelectorConcept.js | 7 ------- src/scripts/SelectorList.js | 3 --- 3 files changed, 1 insertion(+), 14 deletions(-) delete mode 100644 src/scripts/Selector/SelectorConcept.js diff --git a/src/devtools/views/SelectorEdit.html b/src/devtools/views/SelectorEdit.html index c898438..d41a5e7 100644 --- a/src/devtools/views/SelectorEdit.html +++ b/src/devtools/views/SelectorEdit.html @@ -32,14 +32,11 @@
+
-
- -
-