diff --git a/apps/admin-x-settings/src/components/settings/advanced/labs/AlphaFeatures.tsx b/apps/admin-x-settings/src/components/settings/advanced/labs/AlphaFeatures.tsx index 41fbafc712..a3cc42f981 100644 --- a/apps/admin-x-settings/src/components/settings/advanced/labs/AlphaFeatures.tsx +++ b/apps/admin-x-settings/src/components/settings/advanced/labs/AlphaFeatures.tsx @@ -63,6 +63,10 @@ const features = [{ title: 'Internal Linking @-links (internal alpha)', description: 'Adds internal URL search when typing @ in the editor', flag: 'internalLinkingAtLinks' +},{ + title: 'Internal Linking search improvements (internal alpha)', + description: 'Replaces Admin\'s search with flexsearch indexes', + flag: 'internalLinkingSearchImprovements' },{ title: 'ActivityPub', description: '(Highly) Experimental support for ActivityPub.', diff --git a/ghost/admin/app/services/feature.js b/ghost/admin/app/services/feature.js index f9c6010f5d..c03ea86c3a 100644 --- a/ghost/admin/app/services/feature.js +++ b/ghost/admin/app/services/feature.js @@ -84,6 +84,7 @@ export default class FeatureService extends Service { @feature('ActivityPub') ActivityPub; @feature('internalLinking') internalLinking; @feature('internalLinkingAtLinks') internalLinkingAtLinks; + @feature('internalLinkingSearchImprovements') internalLinkingSearchImprovements; @feature('editorExcerpt') editorExcerpt; @feature('newsletterExcerpt') newsletterExcerpt; diff --git a/ghost/admin/app/services/search-provider-beta.js b/ghost/admin/app/services/search-provider-beta.js new file mode 100644 index 0000000000..e69fa24780 --- /dev/null +++ b/ghost/admin/app/services/search-provider-beta.js @@ -0,0 +1,136 @@ +import RSVP from 'rsvp'; +import Service from '@ember/service'; +import {default as Flexsearch} from 'flexsearch'; +import {isEmpty} from '@ember/utils'; +import {pluralize} from 'ember-inflector'; +import {inject as service} from '@ember/service'; +import {task} from 'ember-concurrency'; + +const {Document} = Flexsearch; + +export const SEARCHABLES = [ + { + name: 'Staff', + model: 'user', + fields: ['id', 'slug', 'url', 'name', 'profile_image'], + pathField: 'slug', + titleField: 'name', + index: ['name'] + }, + { + name: 'Tags', + model: 'tag', + fields: ['id', 'slug', 'url', 'name'], + pathField: 'slug', + titleField: 'name', + index: ['name'] + }, + { + name: 'Posts', + model: 'post', + fields: ['id', 'url', 'title', 'excerpt', 'status', 'published_at', 'visibility'], + pathField: 'id', + titleField: 'title', + index: ['title', 'excerpt'] + }, + { + name: 'Pages', + model: 'page', + fields: ['id', 'url', 'title', 'excerpt', 'status', 'published_at', 'visibility'], + pathField: 'id', + titleField: 'title', + index: ['title', 'excerpt'] + } +]; + +export default class SearchProviderService extends Service { + @service ajax; + @service notifications; + @service store; + + indexes = SEARCHABLES.reduce((indexes, searchable) => { + indexes[searchable.model] = new Document({ + tokenize: 'forward', + document: { + id: 'id', + index: searchable.index, + store: true + } + }); + + return indexes; + }, {}); + + /* eslint-disable require-yield */ + @task + *searchTask(term) { + const results = []; + + SEARCHABLES.forEach((searchable) => { + const searchResults = this.indexes[searchable.model].search(term, {enrich: true}); + const usedIds = new Set(); + const groupResults = []; + + searchResults.forEach((field) => { + field.result.forEach((searchResult) => { + const {id, doc} = searchResult; + + if (usedIds.has(id)) { + return; + } + + usedIds.add(id); + + groupResults.push({ + id: `${searchable.model}.${doc[searchable.pathField]}`, + title: doc[searchable.titleField], + groupName: searchable.name, + status: doc.status, + visibility: doc.visibility, + publishedAt: doc.published_at + }); + }); + }); + + if (!isEmpty(groupResults)) { + results.push({ + groupName: searchable.name, + options: groupResults + }); + } + }); + + return results; + } + /* eslint-enable require-yield */ + + @task + *refreshContentTask() { + try { + const promises = SEARCHABLES.map(searchable => this.#loadSearchable(searchable)); + yield RSVP.all(promises); + } catch (error) { + // eslint-disable-next-line + console.error(error); + } + } + + async #loadSearchable(searchable) { + const url = `${this.store.adapterFor(searchable.model).urlForQuery({}, searchable.model)}/`; + const query = {fields: searchable.fields, limit: 10000}; + + try { + const response = await this.ajax.request(url, {data: query}); + + response[pluralize(searchable.model)].forEach((item) => { + this.indexes[searchable.model].add(item); + }); + } catch (error) { + console.error(error); // eslint-disable-line + + this.notifications.showAPIError(error, { + key: `search.load${searchable.name}.error` + }); + } + } +} diff --git a/ghost/admin/app/services/search-provider.js b/ghost/admin/app/services/search-provider.js new file mode 100644 index 0000000000..2bef46af0c --- /dev/null +++ b/ghost/admin/app/services/search-provider.js @@ -0,0 +1,116 @@ +import RSVP from 'rsvp'; +import Service from '@ember/service'; +import {isEmpty} from '@ember/utils'; +import {pluralize} from 'ember-inflector'; +import {inject as service} from '@ember/service'; +import {task} from 'ember-concurrency'; + +export const SEARCHABLES = [ + { + name: 'Staff', + model: 'user', + fields: ['id', 'slug', 'url', 'name'], // id not used but required for API to have correct url + idField: 'slug', + titleField: 'name' + }, + { + name: 'Tags', + model: 'tag', + fields: ['slug', 'url', 'name'], + idField: 'slug', + titleField: 'name' + }, + { + name: 'Posts', + model: 'post', + fields: ['id', 'url', 'title', 'status', 'published_at', 'visibility'], + idField: 'id', + titleField: 'title' + }, + { + name: 'Pages', + model: 'page', + fields: ['id', 'url', 'title', 'status', 'published_at', 'visibility'], + idField: 'id', + titleField: 'title' + } +]; + +export default class SearchProviderService extends Service { + @service ajax; + @service notifications; + @service store; + + content = []; + + /* eslint-disable require-yield */ + @task + *searchTask(term) { + const normalizedTerm = term.toString().toLowerCase(); + const results = []; + + SEARCHABLES.forEach((searchable) => { + const matchedContent = this.content.filter((item) => { + const normalizedTitle = item.title.toString().toLowerCase(); + return ( + item.groupName === searchable.name && + normalizedTitle.indexOf(normalizedTerm) >= 0 + ); + }); + + if (!isEmpty(matchedContent)) { + results.push({ + groupName: searchable.name, + options: matchedContent + }); + } + }); + + return results; + } + /* eslint-enable require-yield */ + + @task + *refreshContentTask() { + const content = []; + const promises = SEARCHABLES.map(searchable => this._loadSearchable(searchable, content)); + + try { + yield RSVP.all(promises); + this.content = content; + } catch (error) { + // eslint-disable-next-line + console.error(error); + } + } + + async _loadSearchable(searchable, content) { + const url = `${this.store.adapterFor(searchable.model).urlForQuery({}, searchable.model)}/`; + const maxSearchableLimit = '10000'; + const query = {fields: searchable.fields, limit: maxSearchableLimit}; + + try { + const response = await this.ajax.request(url, {data: query}); + + const items = response[pluralize(searchable.model)].map( + item => ({ + id: `${searchable.model}.${item[searchable.idField]}`, + url: item.url, + title: item[searchable.titleField], + groupName: searchable.name, + status: item.status, + visibility: item.visibility, + publishedAt: item.published_at + }) + ); + + content.push(...items); + } catch (error) { + console.error(error); // eslint-disable-line + + this.notifications.showAPIError(error, { + key: `search.load${searchable.name}.error` + }); + } + } +} diff --git a/ghost/admin/app/services/search.js b/ghost/admin/app/services/search.js index 9b5ea0508b..c90b6bef02 100644 --- a/ghost/admin/app/services/search.js +++ b/ghost/admin/app/services/search.js @@ -1,49 +1,24 @@ -import RSVP from 'rsvp'; import Service from '@ember/service'; import {action} from '@ember/object'; -import {isBlank, isEmpty} from '@ember/utils'; -import {pluralize} from 'ember-inflector'; +import {isBlank} from '@ember/utils'; import {inject as service} from '@ember/service'; import {task, timeout} from 'ember-concurrency'; export default class SearchService extends Service { @service ajax; + @service feature; @service notifications; + @service searchProvider; + @service searchProviderBeta; @service store; - content = []; isContentStale = true; - searchables = [ - { - name: 'Staff', - model: 'user', - fields: ['id', 'slug', 'url', 'name'], // id not used but required for API to have correct url - idField: 'slug', - titleField: 'name' - }, - { - name: 'Tags', - model: 'tag', - fields: ['slug', 'url', 'name'], - idField: 'slug', - titleField: 'name' - }, - { - name: 'Posts', - model: 'post', - fields: ['id', 'url', 'title', 'status', 'published_at', 'visibility'], - idField: 'id', - titleField: 'title' - }, - { - name: 'Pages', - model: 'page', - fields: ['id', 'url', 'title', 'status', 'published_at', 'visibility'], - idField: 'id', - titleField: 'title' - } - ]; + get provider() { + return this.feature.internalLinkingSearchImprovements + ? this.searchProviderBeta + : this.searchProvider; + } @action expireContent() { @@ -67,33 +42,7 @@ export default class SearchService extends Service { yield this.refreshContentTask.lastRunning; } - const searchResult = this._searchContent(term); - - return searchResult; - } - - _searchContent(term) { - const normalizedTerm = term.toString().toLowerCase(); - const results = []; - - this.searchables.forEach((searchable) => { - const matchedContent = this.content.filter((item) => { - const normalizedTitle = item.title.toString().toLowerCase(); - return ( - item.groupName === searchable.name && - normalizedTitle.indexOf(normalizedTerm) >= 0 - ); - }); - - if (!isEmpty(matchedContent)) { - results.push({ - groupName: searchable.name, - options: matchedContent - }); - } - }); - - return results; + return yield this.provider.searchTask.perform(term); } @task({drop: true}) @@ -104,47 +53,8 @@ export default class SearchService extends Service { this.isContentStale = true; - const content = []; - const promises = this.searchables.map(searchable => this._loadSearchable(searchable, content)); - - try { - yield RSVP.all(promises); - this.content = content; - } catch (error) { - // eslint-disable-next-line - console.error(error); - } + yield this.provider.refreshContentTask.perform(); this.isContentStale = false; } - - async _loadSearchable(searchable, content) { - const url = `${this.store.adapterFor(searchable.model).urlForQuery({}, searchable.model)}/`; - const maxSearchableLimit = '10000'; - const query = {fields: searchable.fields, limit: maxSearchableLimit}; - - try { - const response = await this.ajax.request(url, {data: query}); - - const items = response[pluralize(searchable.model)].map( - item => ({ - id: `${searchable.model}.${item[searchable.idField]}`, - url: item.url, - title: item[searchable.titleField], - groupName: searchable.name, - status: item.status, - visibility: item.visibility, - publishedAt: item.published_at - }) - ); - - content.push(...items); - } catch (error) { - console.error(error); // eslint-disable-line - - this.notifications.showAPIError(error, { - key: `search.load${searchable.name}.error` - }); - } - } } diff --git a/ghost/admin/package.json b/ghost/admin/package.json index 182b6d2f40..3933f4825c 100644 --- a/ghost/admin/package.json +++ b/ghost/admin/package.json @@ -123,6 +123,7 @@ "ember-websockets": "10.2.1", "eslint-plugin-babel": "5.3.1", "faker": "5.5.3", + "flexsearch": "0.7.43", "fs-extra": "11.2.0", "glob": "8.1.0", "google-caja-bower": "https://github.com/acburdine/google-caja-bower#ghost", @@ -205,4 +206,4 @@ } } } -} \ No newline at end of file +} diff --git a/ghost/admin/tests/acceptance/search-test.js b/ghost/admin/tests/acceptance/search-test.js index 47af1f8d2b..3e025285be 100644 --- a/ghost/admin/tests/acceptance/search-test.js +++ b/ghost/admin/tests/acceptance/search-test.js @@ -2,167 +2,184 @@ import ctrlOrCmd from 'ghost-admin/utils/ctrl-or-cmd'; import {authenticateSession} from 'ember-simple-auth/test-support'; import {click, currentURL, find, findAll, triggerKeyEvent, visit} from '@ember/test-helpers'; import {describe, it} from 'mocha'; +import {enableLabsFlag} from '../helpers/labs-flag'; import {expect} from 'chai'; import {getPosts} from '../../mirage/config/posts'; import {setupApplicationTest} from 'ember-mocha'; import {setupMirage} from 'ember-cli-mirage/test-support'; import {typeInSearch} from 'ember-power-select/test-support/helpers'; -describe('Acceptance: Search', function () { - const trigger = '[data-test-modal="search"] .ember-power-select-trigger'; - // eslint-disable-next-line no-unused-vars - let firstUser, firstPost, secondPost, firstPage, firstTag; +const suites = [{ + name: 'Acceptance: Search', + beforeEach() { + // noop + } +}, { + name: 'Acceptance: Search (beta)', + beforeEach() { + enableLabsFlag(this.server, 'internalLinking'); + } +}]; - const hooks = setupApplicationTest(); - setupMirage(hooks); +suites.forEach((suite) => { + describe(suite.name, function () { + const trigger = '[data-test-modal="search"] .ember-power-select-trigger'; + // eslint-disable-next-line no-unused-vars + let firstUser, firstPost, secondPost, firstPage, firstTag; - this.beforeEach(async function () { - this.server.loadFixtures(); + const hooks = setupApplicationTest(); + setupMirage(hooks); - // create user to authenticate as - let role = this.server.create('role', {name: 'Owner'}); - firstUser = this.server.create('user', {roles: [role], slug: 'owner', name: 'First user'}); + this.beforeEach(async function () { + this.server.loadFixtures(); - // populate store with data we'll be searching - firstPost = this.server.create('post', {title: 'First post', slug: 'first-post'}); - secondPost = this.server.create('post', {title: 'Second post', slug: 'second-post'}); - firstPage = this.server.create('page', {title: 'First page', slug: 'first-page'}); - firstTag = this.server.create('tag', {name: 'First tag', slug: 'first-tag'}); + // create user to authenticate as + let role = this.server.create('role', {name: 'Owner'}); + firstUser = this.server.create('user', {roles: [role], slug: 'owner', name: 'First user'}); - return await authenticateSession(); - }); + // populate store with data we'll be searching + firstPost = this.server.create('post', {title: 'First post', slug: 'first-post'}); + secondPost = this.server.create('post', {title: 'Second post', slug: 'second-post'}); + firstPage = this.server.create('page', {title: 'First page', slug: 'first-page'}); + firstTag = this.server.create('tag', {name: 'First tag', slug: 'first-tag'}); - it('opens search modal when clicking icon', async function () { - await visit('/dashboard'); - expect(currentURL(), 'currentURL').to.equal('/dashboard'); - expect(find('[data-test-modal="search"]'), 'search modal').to.not.exist; - await click('[data-test-button="search"]'); - expect(find('[data-test-modal="search"]'), 'search modal').to.exist; - }); + suite.beforeEach.bind(this)(); - it('opens search icon when pressing Ctrl/Cmd+K', async function () { - await visit('/dashboard'); - expect(find('[data-test-modal="search"]'), 'search modal').to.not.exist; - await triggerKeyEvent(document, 'keydown', 'K', { - metaKey: ctrlOrCmd === 'command', - ctrlKey: ctrlOrCmd === 'ctrl' + return await authenticateSession(); }); - expect(find('[data-test-modal="search"]'), 'search modal').to.exist; - }); - it('closes search modal on escape key', async function () { - await visit('/dashboard'); - await click('[data-test-button="search"]'); - expect(find('[data-test-modal="search"]'), 'search modal').to.exist; - await triggerKeyEvent(document, 'keydown', 'Escape'); - expect(find('[data-test-modal="search"]'), 'search modal').to.not.exist; - }); + it('opens search modal when clicking icon', async function () { + await visit('/dashboard'); + expect(currentURL(), 'currentURL').to.equal('/dashboard'); + expect(find('[data-test-modal="search"]'), 'search modal').to.not.exist; + await click('[data-test-button="search"]'); + expect(find('[data-test-modal="search"]'), 'search modal').to.exist; + }); - it('closes search modal on click outside', async function () { - await visit('/dashboard'); - await click('[data-test-button="search"]'); - expect(find('[data-test-modal="search"]'), 'search modal').to.exist; - await click('.epm-backdrop'); - expect(find('[data-test-modal="search"]'), 'search modal').to.not.exist; - }); + it('opens search icon when pressing Ctrl/Cmd+K', async function () { + await visit('/dashboard'); + expect(find('[data-test-modal="search"]'), 'search modal').to.not.exist; + await triggerKeyEvent(document, 'keydown', 'K', { + metaKey: ctrlOrCmd === 'command', + ctrlKey: ctrlOrCmd === 'ctrl' + }); + expect(find('[data-test-modal="search"]'), 'search modal').to.exist; + }); - it('finds posts, pages, staff, and tags when typing', async function () { - await visit('/dashboard'); - await click('[data-test-button="search"]'); - await typeInSearch('first'); // search is not case sensitive + it('closes search modal on escape key', async function () { + await visit('/dashboard'); + await click('[data-test-button="search"]'); + expect(find('[data-test-modal="search"]'), 'search modal').to.exist; + await triggerKeyEvent(document, 'keydown', 'Escape'); + expect(find('[data-test-modal="search"]'), 'search modal').to.not.exist; + }); - // all groups are present - const groupNames = findAll('.ember-power-select-group-name'); - expect(groupNames, 'group names').to.have.length(4); - expect(groupNames.map(el => el.textContent.trim())).to.deep.equal(['Staff', 'Tags', 'Posts', 'Pages']); + it('closes search modal on click outside', async function () { + await visit('/dashboard'); + await click('[data-test-button="search"]'); + expect(find('[data-test-modal="search"]'), 'search modal').to.exist; + await click('.epm-backdrop'); + expect(find('[data-test-modal="search"]'), 'search modal').to.not.exist; + }); - // correct results are found - const options = findAll('.ember-power-select-option'); - expect(options, 'number of search results').to.have.length(4); - expect(options.map(el => el.textContent.trim())).to.deep.equal(['First user', 'First tag', 'First post', 'First page']); + it('finds posts, pages, staff, and tags when typing', async function () { + await visit('/dashboard'); + await click('[data-test-button="search"]'); + await typeInSearch('first'); // search is not case sensitive - // first item is selected - expect(options[0]).to.have.attribute('aria-current', 'true'); - }); + // all groups are present + const groupNames = findAll('.ember-power-select-group-name'); + expect(groupNames, 'group names').to.have.length(4); + expect(groupNames.map(el => el.textContent.trim())).to.deep.equal(['Staff', 'Tags', 'Posts', 'Pages']); - it('up/down arrows move selected item', async function () { - await visit('/dashboard'); - await click('[data-test-button="search"]'); - await typeInSearch('first post'); - expect(findAll('.ember-power-select-option')[0], 'first option (initial)').to.have.attribute('aria-current', 'true'); - await triggerKeyEvent(trigger, 'keyup', 'ArrowDown'); - expect(findAll('.ember-power-select-option')[0], 'second option (after down)').to.have.attribute('aria-current', 'true'); - await triggerKeyEvent(trigger, 'keyup', 'ArrowUp'); - expect(findAll('.ember-power-select-option')[0], 'first option (after up)').to.have.attribute('aria-current', 'true'); - }); + // correct results are found + const options = findAll('.ember-power-select-option'); + expect(options, 'number of search results').to.have.length(4); + expect(options.map(el => el.textContent.trim())).to.deep.equal(['First user', 'First tag', 'First post', 'First page']); - it('navigates to editor when post selected (Enter)', async function () { - await visit('/dashboard'); - await click('[data-test-button="search"]'); - await typeInSearch('first post'); - await triggerKeyEvent(trigger, 'keydown', 'Enter'); - expect(currentURL(), 'url after selecting post').to.equal(`/editor/post/${firstPost.id}`); - }); + // first item is selected + expect(options[0]).to.have.attribute('aria-current', 'true'); + }); - it('navigates to editor when post selected (Clicked)', async function () { - await visit('/dashboard'); - await click('[data-test-button="search"]'); - await typeInSearch('first post'); - await click('.ember-power-select-option[aria-current="true"]'); - expect(currentURL(), 'url after selecting post').to.equal(`/editor/post/${firstPost.id}`); - }); + it('up/down arrows move selected item', async function () { + await visit('/dashboard'); + await click('[data-test-button="search"]'); + await typeInSearch('first post'); + expect(findAll('.ember-power-select-option')[0], 'first option (initial)').to.have.attribute('aria-current', 'true'); + await triggerKeyEvent(trigger, 'keyup', 'ArrowDown'); + expect(findAll('.ember-power-select-option')[0], 'second option (after down)').to.have.attribute('aria-current', 'true'); + await triggerKeyEvent(trigger, 'keyup', 'ArrowUp'); + expect(findAll('.ember-power-select-option')[0], 'first option (after up)').to.have.attribute('aria-current', 'true'); + }); - it('navigates to editor when page selected', async function () { - await visit('/dashboard'); - await click('[data-test-button="search"]'); - await typeInSearch('page'); - await triggerKeyEvent(trigger, 'keydown', 'Enter'); - expect(currentURL(), 'url after selecting page').to.equal(`/editor/page/${firstPage.id}`); - }); + it('navigates to editor when post selected (Enter)', async function () { + await visit('/dashboard'); + await click('[data-test-button="search"]'); + await typeInSearch('first post'); + await triggerKeyEvent(trigger, 'keydown', 'Enter'); + expect(currentURL(), 'url after selecting post').to.equal(`/editor/post/${firstPost.id}`); + }); - it('navigates to tag edit screen when tag selected', async function () { - await visit('/dashboard'); - await click('[data-test-button="search"]'); - await typeInSearch('tag'); - await triggerKeyEvent(trigger, 'keydown', 'Enter'); - expect(currentURL(), 'url after selecting tag').to.equal(`/tags/${firstTag.slug}`); - }); + it('navigates to editor when post selected (Clicked)', async function () { + await visit('/dashboard'); + await click('[data-test-button="search"]'); + await typeInSearch('first post'); + await click('.ember-power-select-option[aria-current="true"]'); + expect(currentURL(), 'url after selecting post').to.equal(`/editor/post/${firstPost.id}`); + }); - // TODO: Staff settings are now part of AdminX so this isn't working, can we test AdminX from Ember tests? - it.skip('navigates to user edit screen when user selected', async function () { - await visit('/dashboard'); - await click('[data-test-button="search"]'); - await typeInSearch('user'); - await triggerKeyEvent(trigger, 'keydown', 'Enter'); - expect(currentURL(), 'url after selecting user').to.equal(`/settings/staff/${firstUser.slug}`); - }); + it('navigates to editor when page selected', async function () { + await visit('/dashboard'); + await click('[data-test-button="search"]'); + await typeInSearch('page'); + await triggerKeyEvent(trigger, 'keydown', 'Enter'); + expect(currentURL(), 'url after selecting page').to.equal(`/editor/page/${firstPage.id}`); + }); - it('shows no results message when no results', async function () { - await visit('/dashboard'); - await click('[data-test-button="search"]'); - await typeInSearch('x'); - expect(find('.ember-power-select-option--no-matches-message'), 'no results message').to.contain.text('No results found'); - }); + it('navigates to tag edit screen when tag selected', async function () { + await visit('/dashboard'); + await click('[data-test-button="search"]'); + await typeInSearch('tag'); + await triggerKeyEvent(trigger, 'keydown', 'Enter'); + expect(currentURL(), 'url after selecting tag').to.equal(`/tags/${firstTag.slug}`); + }); - // https://linear.app/tryghost/issue/MOM-103/search-stalls-on-query-when-refresh-occurs - it('handles refresh on first search being slow', async function () { - this.server.get('/posts/', getPosts, {timing: 200}); + // TODO: Staff settings are now part of AdminX so this isn't working, can we test AdminX from Ember tests? + it.skip('navigates to user edit screen when user selected', async function () { + await visit('/dashboard'); + await click('[data-test-button="search"]'); + await typeInSearch('user'); + await triggerKeyEvent(trigger, 'keydown', 'Enter'); + expect(currentURL(), 'url after selecting user').to.equal(`/settings/staff/${firstUser.slug}`); + }); - await visit('/dashboard'); - await click('[data-test-button="search"]'); - await typeInSearch('first'); // search is not case sensitive + it('shows no results message when no results', async function () { + await visit('/dashboard'); + await click('[data-test-button="search"]'); + await typeInSearch('x'); + expect(find('.ember-power-select-option--no-matches-message'), 'no results message').to.contain.text('No results found'); + }); - // all groups are present - const groupNames = findAll('.ember-power-select-group-name'); - expect(groupNames, 'group names').to.have.length(4); - expect(groupNames.map(el => el.textContent.trim())).to.deep.equal(['Staff', 'Tags', 'Posts', 'Pages']); + // https://linear.app/tryghost/issue/MOM-103/search-stalls-on-query-when-refresh-occurs + it('handles refresh on first search being slow', async function () { + this.server.get('/posts/', getPosts, {timing: 200}); - // correct results are found - const options = findAll('.ember-power-select-option'); - expect(options, 'number of search results').to.have.length(4); - expect(options.map(el => el.textContent.trim())).to.deep.equal(['First user', 'First tag', 'First post', 'First page']); + await visit('/dashboard'); + await click('[data-test-button="search"]'); + await typeInSearch('first'); // search is not case sensitive - // first item is selected - expect(options[0]).to.have.attribute('aria-current', 'true'); + // all groups are present + const groupNames = findAll('.ember-power-select-group-name'); + expect(groupNames, 'group names').to.have.length(4); + expect(groupNames.map(el => el.textContent.trim())).to.deep.equal(['Staff', 'Tags', 'Posts', 'Pages']); + + // correct results are found + const options = findAll('.ember-power-select-option'); + expect(options, 'number of search results').to.have.length(4); + expect(options.map(el => el.textContent.trim())).to.deep.equal(['First user', 'First tag', 'First post', 'First page']); + + // first item is selected + expect(options[0]).to.have.attribute('aria-current', 'true'); + }); }); }); diff --git a/ghost/core/core/shared/labs.js b/ghost/core/core/shared/labs.js index 2d1a5446b4..6cc406e0a7 100644 --- a/ghost/core/core/shared/labs.js +++ b/ghost/core/core/shared/labs.js @@ -54,7 +54,8 @@ const ALPHA_FEATURES = [ 'importMemberTier', 'lexicalIndicators', 'adminXDemo', - 'internalLinkingAtLinks' + 'internalLinkingAtLinks', + 'internalLinkingSearchImprovements' ]; module.exports.GA_KEYS = [...GA_FEATURES]; diff --git a/yarn.lock b/yarn.lock index 4b9ad47ee1..ccb0036c60 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2430,7 +2430,7 @@ ember-cli-babel "^7.22.1" ember-compatibility-helpers "^1.1.1" -"@ember/render-modifiers@2.1.0", "@ember/render-modifiers@^1.0.2 || ^2.0.0", "@ember/render-modifiers@^2.0.0", "@ember/render-modifiers@^2.0.4": +"@ember/render-modifiers@2.1.0", "@ember/render-modifiers@^1.0.2 || ^2.0.0", "@ember/render-modifiers@^2.0.4": version "2.1.0" resolved "https://registry.yarnpkg.com/@ember/render-modifiers/-/render-modifiers-2.1.0.tgz#f4fff95a8b5cfbe947ec46644732d511711c5bf9" integrity sha512-LruhfoDv2itpk0fA0IC76Sxjcnq/7BC6txpQo40hOko8Dn6OxwQfxkPIbZGV0Cz7df+iX+VJrcYzNIvlc3w2EQ== @@ -2568,19 +2568,6 @@ resolve "^1.8.1" semver "^7.3.2" -"@embroider/macros@0.47.2", "@embroider/macros@^0.47.2": - version "0.47.2" - resolved "https://registry.yarnpkg.com/@embroider/macros/-/macros-0.47.2.tgz#23cbe92cac3c24747f054e1eea2a22538bf7ebd0" - integrity sha512-ViNWluJCeM5OPlM3rs8kdOz3RV5rpfXX5D2rDnc/q86xRS0xf4NFEjYRV7W6fBcD0b3v5jSHDTwrjq9Kee4rHg== - dependencies: - "@embroider/shared-internals" "0.47.2" - assert-never "^1.2.1" - ember-cli-babel "^7.26.6" - find-up "^5.0.0" - lodash "^4.17.21" - resolve "^1.20.0" - semver "^7.3.2" - "@embroider/macros@1.13.4", "@embroider/macros@^0.50.0 || ^1.0.0", "@embroider/macros@^1.0.0", "@embroider/macros@^1.10.0", "@embroider/macros@^1.12.2", "@embroider/macros@^1.2.0", "@embroider/macros@^1.8.0", "@embroider/macros@^1.8.3", "@embroider/macros@^1.9.0": version "1.13.4" resolved "https://registry.yarnpkg.com/@embroider/macros/-/macros-1.13.4.tgz#4fefb79d68bcfbc4619551572b2ca3040a224fb5" @@ -2608,19 +2595,6 @@ semver "^7.3.2" typescript-memoize "^1.0.0-alpha.3" -"@embroider/shared-internals@0.47.2": - version "0.47.2" - resolved "https://registry.yarnpkg.com/@embroider/shared-internals/-/shared-internals-0.47.2.tgz#24e9fa0dd9c529d5c996ee1325729ea08d1fa19f" - integrity sha512-SxdZYjAE0fiM5zGDz+12euWIsQZ1tsfR1k+NKmiWMyLhA5T3pNgbR2/Djvx/cVIxOtEavGGSllYbzRKBtV4xMg== - dependencies: - babel-import-util "^0.2.0" - ember-rfc176-data "^0.3.17" - fs-extra "^9.1.0" - lodash "^4.17.21" - resolve-package-path "^4.0.1" - semver "^7.3.5" - typescript-memoize "^1.0.1" - "@embroider/shared-internals@2.5.1", "@embroider/shared-internals@^2.0.0": version "2.5.1" resolved "https://registry.yarnpkg.com/@embroider/shared-internals/-/shared-internals-2.5.1.tgz#a4d8c057cbff293ef6eb29ee6537f263d206b444" @@ -2681,15 +2655,6 @@ broccoli-funnel "^3.0.5" ember-cli-babel "^7.23.1" -"@embroider/util@^0.47.2": - version "0.47.2" - resolved "https://registry.yarnpkg.com/@embroider/util/-/util-0.47.2.tgz#d06497b4b84c07ed9c7b628293bb019c533f4556" - integrity sha512-g9OqnFJPktGu9NS0Ug3Pxz1JE3jeDceeVE4IrlxDrVmBXMA/GrBvpwjolWgl6jh97cMJyExdz62jIvPHV4256Q== - dependencies: - "@embroider/macros" "0.47.2" - broccoli-funnel "^3.0.5" - ember-cli-babel "^7.23.1" - "@emotion/babel-plugin@^11.11.0": version "11.11.0" resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz#c2d872b6a7767a9d176d007f5b31f7d504bb5d6c" @@ -3347,6 +3312,18 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== + dependencies: + string-width "^5.1.2" + string-width-cjs "npm:string-width@^4.2.0" + strip-ansi "^7.0.1" + strip-ansi-cjs "npm:strip-ansi@^6.0.1" + wrap-ansi "^8.1.0" + wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" + "@isaacs/ttlcache@1.4.1": version "1.4.1" resolved "https://registry.yarnpkg.com/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz#21fb23db34e9b6220c6ba023a0118a2dd3461ea2" @@ -6952,7 +6929,7 @@ "@tryghost/root-utils" "^0.3.28" debug "^4.3.1" -"@tryghost/elasticsearch@^3.0.19": +"@tryghost/elasticsearch@^3.0.16", "@tryghost/elasticsearch@^3.0.19": version "3.0.19" resolved "https://registry.yarnpkg.com/@tryghost/elasticsearch/-/elasticsearch-3.0.19.tgz#a0a94b667c83575a57775027aea5cb4ff4f216ef" integrity sha512-7yOPEnkebsMuHeIH5oYRQoHa1vz1AkjHRPN4GWJhcywrH20S3Kj66oUPj+8jyqDOvfwqPc7Vfa6jc7rTcy3AQQ== @@ -6982,7 +6959,15 @@ focus-trap "^6.7.2" postcss-preset-env "^7.3.1" -"@tryghost/errors@1.3.1", "@tryghost/errors@1.3.2", "@tryghost/errors@^1.2.26", "@tryghost/errors@^1.2.3", "@tryghost/errors@^1.3.2": +"@tryghost/errors@1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@tryghost/errors/-/errors-1.3.1.tgz#32a00c5e5293c46e54d03a66da871ac34b2ab35c" + integrity sha512-iZqT0vZ3NVZNq9o1HYxW00k1mcUAC+t5OLiI8O29/uQwAfy7NemY+Cabl9mWoIwgvBmw7l0Z8pHTcXMo1c+xMw== + dependencies: + "@stdlib/utils-copy" "^0.0.7" + uuid "^9.0.0" + +"@tryghost/errors@1.3.2", "@tryghost/errors@^1.2.26", "@tryghost/errors@^1.2.3", "@tryghost/errors@^1.3.2": version "1.3.2" resolved "https://registry.yarnpkg.com/@tryghost/errors/-/errors-1.3.2.tgz#3612f6f59ca07e37d1095f9eb31ff6a069a3b7f2" integrity sha512-Aqi2vz7HHwN6p2juIYUu8vpMtaKavjULBKNnL0l1req9qXjPs90i/HV8zhvK0ceeWuPdEXaCkfHSRr/yxG3/uw== @@ -7020,7 +7005,7 @@ resolved "https://registry.yarnpkg.com/@tryghost/http-cache-utils/-/http-cache-utils-0.1.15.tgz#721ffd1f3b7173da679f6c8c25cda6f0c728264b" integrity sha512-G0x9ZUkBbTWIFg2Dnng6qKLststZKuUYx98pcowNwpYuEGUPOka4eAJj3frz/60F1CzMb1qYy9WEjD/xRGd5oQ== -"@tryghost/http-stream@^0.1.30": +"@tryghost/http-stream@^0.1.27", "@tryghost/http-stream@^0.1.30": version "0.1.30" resolved "https://registry.yarnpkg.com/@tryghost/http-stream/-/http-stream-0.1.30.tgz#ef2a9a55a2ec7fab7e889a11ce23e7ebfaab8a0b" integrity sha512-ZlcwtWQICN2OeHcVAL+BXxkzO5tICdLYZeQ2EuVk2E+mC+1ARFrpVUbpTnIbCXDL+uV3cOLqPh2KNGGJAdDChw== @@ -7206,7 +7191,24 @@ lodash "^4.17.21" luxon "^1.26.0" -"@tryghost/logging@2.4.10", "@tryghost/logging@2.4.15", "@tryghost/logging@^2.4.7": +"@tryghost/logging@2.4.10": + version "2.4.10" + resolved "https://registry.yarnpkg.com/@tryghost/logging/-/logging-2.4.10.tgz#2e5b56c53364be330c1e6f2ffa33e3c30b7bac8e" + integrity sha512-l356vLSQmszY14y7ef5YxY4CZ3418NXn5+LvFdlweeTRk0ilWx1mVUoXi8IlVh90rIVbemv+pXi1dusJB6peQA== + dependencies: + "@tryghost/bunyan-rotating-filestream" "^0.0.7" + "@tryghost/elasticsearch" "^3.0.16" + "@tryghost/http-stream" "^0.1.27" + "@tryghost/pretty-stream" "^0.1.21" + "@tryghost/root-utils" "^0.3.25" + bunyan "^1.8.15" + bunyan-loggly "^1.4.2" + fs-extra "^11.0.0" + gelf-stream "^1.1.1" + json-stringify-safe "^5.0.1" + lodash "^4.17.21" + +"@tryghost/logging@2.4.15", "@tryghost/logging@^2.4.7": version "2.4.15" resolved "https://registry.yarnpkg.com/@tryghost/logging/-/logging-2.4.15.tgz#a94e37d760a62d6f2fc2868e4cd8bf6f219b2a2e" integrity sha512-mSVdSR/9bd1D/DCFpfeFn2AnPE/0lK78ePHBrtteOipA7ogL0Kd+QvabHK5iKLe+/20flBZs4BvnU/DBuS8Pvw== @@ -7295,7 +7297,7 @@ chalk "^4.1.0" sywac "^1.3.0" -"@tryghost/pretty-stream@^0.1.24": +"@tryghost/pretty-stream@^0.1.21", "@tryghost/pretty-stream@^0.1.24": version "0.1.24" resolved "https://registry.yarnpkg.com/@tryghost/pretty-stream/-/pretty-stream-0.1.24.tgz#f670dd66d0d3f7fa733f72294b42fc1b40b0fbd3" integrity sha512-s83KJXIt2nN4JRtBkkWWPLgmh8d/FKd8pvlln4L+EOKkNiVie2vEeATphFsWiK2NZOc4GSgv/fZHxoKBvnOrnA== @@ -7326,7 +7328,7 @@ got "13.0.0" lodash "^4.17.21" -"@tryghost/root-utils@0.3.28", "@tryghost/root-utils@^0.3.24", "@tryghost/root-utils@^0.3.28": +"@tryghost/root-utils@0.3.28", "@tryghost/root-utils@^0.3.24", "@tryghost/root-utils@^0.3.25", "@tryghost/root-utils@^0.3.28": version "0.3.28" resolved "https://registry.yarnpkg.com/@tryghost/root-utils/-/root-utils-0.3.28.tgz#43ae0047927a7753c9b526ea12ce6e382ec7fb1f" integrity sha512-/izwMw9tCJIQ3DVHumzEWgKhKAw5FwTgrrYcCNHl89yijJKaVRBOJUhlB/u2ST6UWfhahodjaYauq7ymTItaeg== @@ -8940,7 +8942,7 @@ ansi-styles@^5.0.0: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== -ansi-styles@^6.0.0, ansi-styles@^6.2.1: +ansi-styles@^6.0.0, ansi-styles@^6.1.0, ansi-styles@^6.2.1: version "6.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== @@ -13976,6 +13978,11 @@ duplexify@^3.4.2, duplexify@^3.5.0, duplexify@^3.6.0: readable-stream "^2.0.0" stream-shift "^1.0.0" +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + ecc-jsbn@~0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" @@ -14161,7 +14168,7 @@ ember-auto-import@2.7.3, "ember-auto-import@^1.12.1 || ^2.4.3", ember-auto-impor typescript-memoize "^1.0.0-alpha.3" walk-sync "^3.0.0" -ember-auto-import@^1.11.2, ember-auto-import@^1.11.3, ember-auto-import@^1.12.0: +ember-auto-import@^1.11.3, ember-auto-import@^1.12.0: version "1.12.2" resolved "https://registry.yarnpkg.com/ember-auto-import/-/ember-auto-import-1.12.2.tgz#cc7298ee5c0654b0249267de68fb27a2861c3579" integrity sha512-gLqML2k77AuUiXxWNon1FSzuG1DV7PEPpCLCU5aJvf6fdL6rmFfElsZRh+8ELEB/qP9dT+LHjNEunVzd2dYc8A== @@ -14196,25 +14203,7 @@ ember-auto-import@^1.11.2, ember-auto-import@^1.11.3, ember-auto-import@^1.12.0: walk-sync "^0.3.3" webpack "^4.43.0" -ember-basic-dropdown@^3.0.11: - version "3.1.0" - resolved "https://registry.yarnpkg.com/ember-basic-dropdown/-/ember-basic-dropdown-3.1.0.tgz#47c292de890d1958057736c00b8eb2b8017d530b" - integrity sha512-UISvgJHfiJ8FeXqH8ZN+NmoImN8p6Sb+85qlEv853hLuEfEYnFUqLNhea8nNl9CpFqcD3yU4dKbhYtc6nB39aQ== - dependencies: - "@ember/render-modifiers" "^2.0.0" - "@embroider/macros" "^0.47.2" - "@embroider/util" "^0.47.2" - "@glimmer/component" "^1.0.4" - "@glimmer/tracking" "^1.0.4" - ember-cli-babel "^7.26.6" - ember-cli-htmlbars "^6.0.0" - ember-cli-typescript "^4.2.1" - ember-element-helper "^0.5.5" - ember-maybe-in-element "^2.0.3" - ember-style-modifier "^0.7.0" - ember-truth-helpers "^2.1.0 || ^3.0.0" - -ember-basic-dropdown@^6.0.0: +ember-basic-dropdown@6.0.2, ember-basic-dropdown@^3.0.11, ember-basic-dropdown@^6.0.0: version "6.0.2" resolved "https://registry.yarnpkg.com/ember-basic-dropdown/-/ember-basic-dropdown-6.0.2.tgz#af47dbd544c605cf9cbc62225185616356aeef52" integrity sha512-JgI/cy7eS/Y2WoQl7B2Mko/1aFTAlxr5d+KpQeH7rBKOFml7IQtLvhiDQrpU/FLkrQ9aLNEJtzwtDZV1xQxAKA== @@ -14844,7 +14833,7 @@ ember-cli@3.24.0: workerpool "^6.0.3" yam "^1.0.0" -ember-compatibility-helpers@^1.1.1, ember-compatibility-helpers@^1.1.2, ember-compatibility-helpers@^1.2.0, ember-compatibility-helpers@^1.2.1, ember-compatibility-helpers@^1.2.4, ember-compatibility-helpers@^1.2.5: +ember-compatibility-helpers@^1.1.1, ember-compatibility-helpers@^1.1.2, ember-compatibility-helpers@^1.2.0, ember-compatibility-helpers@^1.2.1, ember-compatibility-helpers@^1.2.5: version "1.2.7" resolved "https://registry.yarnpkg.com/ember-compatibility-helpers/-/ember-compatibility-helpers-1.2.7.tgz#b4f138bba844f8f38f0b8f4d7e928841cd5e6591" integrity sha512-BtkjulweiXo9c3yVWrtexw2dTmBrvavD/xixNC6TKOBdrixUwU+6nuOO9dufDWsMxoid7MvtmDpzc9+mE8PdaA== @@ -14969,7 +14958,7 @@ ember-drag-drop@0.4.8: dependencies: ember-cli-babel "^6.6.0" -ember-element-helper@^0.5.0, ember-element-helper@^0.5.5: +ember-element-helper@^0.5.0: version "0.5.5" resolved "https://registry.yarnpkg.com/ember-element-helper/-/ember-element-helper-0.5.5.tgz#4a9ecb4dce57ee7f5ceb868a53c7b498c729f056" integrity sha512-Tu3hsI+/mjHBUvw62Qi+YDZtKkn59V66CjwbgfNTZZ7aHf4gFm1ow4zJ4WLnpnie8p9FvOmIUxwl5HvgPJIcFA== @@ -15080,7 +15069,7 @@ ember-in-element-polyfill@^1.0.1: ember-cli-htmlbars "^5.3.1" ember-cli-version-checker "^5.1.2" -ember-in-viewport@4.1.0: +ember-in-viewport@4.1.0, ember-in-viewport@~3.10.2: version "4.1.0" resolved "https://registry.yarnpkg.com/ember-in-viewport/-/ember-in-viewport-4.1.0.tgz#a9359a1e4a99d9d6ab32e926749dc131084ed896" integrity sha512-3y6qWXuJPPc6vX2GfxWgtr+sDjb+bdZF9babstr0lTd8t8c1b42gJ13GaJqlylZIyZz2dEXFCimX9WAeudPv9g== @@ -15094,18 +15083,6 @@ ember-in-viewport@4.1.0: intersection-observer-admin "~0.3.2" raf-pool "~0.1.4" -ember-in-viewport@~3.10.2: - version "3.10.3" - resolved "https://registry.yarnpkg.com/ember-in-viewport/-/ember-in-viewport-3.10.3.tgz#317472bb82bed11f7895821b799349c6a7406e81" - integrity sha512-hSX7p+G6hJjZaY2BAqzyuiMP7QIHzQ4g0+ZBnEwAa8GMbILFAtzPx5A4XEX8wY6dSzhHB9n9jkcWZdmaML6q8A== - dependencies: - ember-auto-import "^1.11.2" - ember-cli-babel "^7.26.3" - ember-modifier "^2.1.0" - fast-deep-equal "^2.0.1" - intersection-observer-admin "~0.3.2" - raf-pool "~0.1.4" - ember-infinity@2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/ember-infinity/-/ember-infinity-2.3.0.tgz#73fe13883c212147bfba4f0b2fe8c8d2a96887d9" @@ -15208,20 +15185,7 @@ ember-modifier@4.1.0, "ember-modifier@^2.1.2 || ^3.0.0 || ^4.0.0", "ember-modifi ember-cli-normalize-entity-name "^1.0.0" ember-cli-string-utils "^1.1.0" -ember-modifier@^2.1.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ember-modifier/-/ember-modifier-2.1.2.tgz#62d18faedf972dcd9d34f90d5321fbc943d139b1" - integrity sha512-3Lsu1fV1sIGa66HOW07RZc6EHISwKt5VA5AUnFss2HX6OTfpxTJ2qvPctt2Yt0XPQXJ4G6BQasr/F35CX7UGJA== - dependencies: - ember-cli-babel "^7.22.1" - ember-cli-normalize-entity-name "^1.0.0" - ember-cli-string-utils "^1.1.0" - ember-cli-typescript "^3.1.3" - ember-compatibility-helpers "^1.2.4" - ember-destroyable-polyfill "^2.0.2" - ember-modifier-manager-polyfill "^1.2.0" - -ember-modifier@^3.0.0, ember-modifier@^3.2.7: +ember-modifier@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/ember-modifier/-/ember-modifier-3.2.7.tgz#f2d35b7c867cbfc549e1acd8d8903c5ecd02ea4b" integrity sha512-ezcPQhH8jUfcJQbbHji4/ZG/h0yyj1jRDknfYue/ypQS8fM8LrGcCMo0rjDZLzL1Vd11InjNs3BD7BdxFlzGoA== @@ -15379,14 +15343,6 @@ ember-source@3.24.0: semver "^6.1.1" silent-error "^1.1.1" -ember-style-modifier@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/ember-style-modifier/-/ember-style-modifier-0.7.0.tgz#85b3dfd7e4bc2bd546df595f2dab4fb141cf7d87" - integrity sha512-iDzffiwJcb9j6gu3g8CxzZOTvRZ0BmLMEFl+uyqjiaj72VVND9+HbLyQRw1/ewPAtinhSktxxTTdwU/JO+stLw== - dependencies: - ember-cli-babel "^7.26.6" - ember-modifier "^3.0.0" - "ember-style-modifier@^0.8.0 || ^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/ember-style-modifier/-/ember-style-modifier-1.0.0.tgz#96e5d342a255d8c1cba1637779dbb1949322e139" @@ -15556,6 +15512,11 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + emojis-list@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" @@ -17200,6 +17161,11 @@ flexsearch@0.7.21: resolved "https://registry.yarnpkg.com/flexsearch/-/flexsearch-0.7.21.tgz#0f5ede3f2aae67ddc351efbe3b24b69d29e9d48b" integrity sha512-W7cHV7Hrwjid6lWmy0IhsWDFQboWSng25U3VVywpHOTJnnAZNPScog67G+cVpeX9f7yDD21ih0WDrMMT+JoaYg== +flexsearch@0.7.43: + version "0.7.43" + resolved "https://registry.yarnpkg.com/flexsearch/-/flexsearch-0.7.43.tgz#34f89b36278a466ce379c5bf6fb341965ed3f16c" + integrity sha512-c5o/+Um8aqCSOXGcZoqZOm+NqtVwNsvVpWv6lfmSclU954O3wvQKxxK8zj74fPaSJbXpSLTs4PRhh+wnoCXnKg== + flow-parser@0.*: version "0.205.1" resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.205.1.tgz#337464aaf027b00b2514610386cf21a5f7c94137" @@ -19943,12 +19909,12 @@ iterate-value@^1.0.2: es-get-iterator "^1.0.2" iterate-iterator "^1.0.1" -jackspeak@2.1.1, jackspeak@^2.3.5: - version "2.1.1" - resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.1.1.tgz#2a42db4cfbb7e55433c28b6f75d8b796af9669cd" - integrity sha512-juf9stUEwUaILepraGOWIJTLwg48bUnBmRqd2ln2Os1sW987zeoj/hzhbvRB95oMuS2ZTpjULmdwHNX4rzZIZw== +jackspeak@^2.3.5: + version "2.3.6" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.3.6.tgz#647ecc472238aee4b06ac0e461acc21a8c505ca8" + integrity sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ== dependencies: - cliui "^8.0.1" + "@isaacs/cliui" "^8.0.2" optionalDependencies: "@pkgjs/parseargs" "^0.11.0" @@ -28208,6 +28174,15 @@ string-template@~0.2.1: resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add" integrity sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw== +"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string-width@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" @@ -28217,15 +28192,6 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - string-width@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" @@ -28234,6 +28200,15 @@ string-width@^2.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" +string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + string-width@^7.0.0: version "7.1.0" resolved "https://registry.yarnpkg.com/string-width/-/string-width-7.1.0.tgz#d994252935224729ea3719c49f7206dc9c46550a" @@ -28314,6 +28289,13 @@ stringify-entities@^2.0.0: is-decimal "^1.0.2" is-hexadecimal "^1.0.0" +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^3.0.0, strip-ansi@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" @@ -28335,14 +28317,7 @@ strip-ansi@^5.1.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^7.1.0: +strip-ansi@^7.0.1, strip-ansi@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== @@ -30780,6 +30755,15 @@ workerpool@^6.0.2, workerpool@^6.0.3, workerpool@^6.1.5, workerpool@^6.4.0: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.5.1.tgz#060f73b39d0caf97c6db64da004cd01b4c099544" integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA== +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^6.0.1: version "6.2.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" @@ -30789,14 +30773,14 @@ wrap-ansi@^6.0.1: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== +wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" wrap-ansi@^9.0.0: version "9.0.0"