mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-10 03:37:43 +02:00
UBERF-4165: Add search to actions popup (#4057)
Signed-off-by: Maxim Karmatskikh <mkarmatskih@gmail.com>
This commit is contained in:
@@ -741,7 +741,7 @@ export function createModel (builder: Builder): void {
|
||||
label: contact.string.SearchPerson,
|
||||
title: contact.string.People,
|
||||
query: contact.completion.PersonQuery,
|
||||
context: ['search', 'mention'],
|
||||
context: ['search', 'mention', 'spotlight'],
|
||||
classToSearch: contact.class.Person
|
||||
},
|
||||
contact.completion.PersonCategory
|
||||
@@ -755,7 +755,7 @@ export function createModel (builder: Builder): void {
|
||||
label: contact.string.SearchOrganization,
|
||||
title: contact.string.Organizations,
|
||||
query: contact.completion.OrganizationQuery,
|
||||
context: ['search', 'mention'],
|
||||
context: ['search', 'mention', 'spotlight'],
|
||||
classToSearch: contact.class.Organization
|
||||
},
|
||||
contact.completion.OrganizationCategory
|
||||
|
||||
@@ -1200,7 +1200,7 @@ export function createModel (builder: Builder): void {
|
||||
label: recruit.string.SearchApplication,
|
||||
title: recruit.string.Applications,
|
||||
query: recruit.completion.ApplicationQuery,
|
||||
context: ['search', 'mention'],
|
||||
context: ['search', 'mention', 'spotlight'],
|
||||
classToSearch: recruit.class.Applicant
|
||||
},
|
||||
recruit.completion.ApplicationCategory
|
||||
@@ -1214,7 +1214,7 @@ export function createModel (builder: Builder): void {
|
||||
label: recruit.string.SearchVacancy,
|
||||
title: recruit.string.Vacancies,
|
||||
query: recruit.completion.VacancyQuery,
|
||||
context: ['search', 'mention'],
|
||||
context: ['search', 'mention', 'spotlight'],
|
||||
classToSearch: recruit.class.Vacancy
|
||||
},
|
||||
recruit.completion.VacancyCategory
|
||||
|
||||
@@ -509,7 +509,7 @@ export function createModel (builder: Builder): void {
|
||||
label: tracker.string.SearchIssue,
|
||||
title: tracker.string.Issues,
|
||||
query: tracker.completion.IssueQuery,
|
||||
context: ['search', 'mention'],
|
||||
context: ['search', 'mention', 'spotlight'],
|
||||
classToSearch: tracker.class.Issue
|
||||
},
|
||||
tracker.completion.IssueCategory
|
||||
|
||||
@@ -56,7 +56,7 @@ export type ObjectSearchFactory = (
|
||||
* search - show in search popup
|
||||
* mention - show in mentions
|
||||
*/
|
||||
export type ObjectSearchContext = 'search' | 'mention'
|
||||
export type ObjectSearchContext = 'search' | 'mention' | 'spotlight'
|
||||
|
||||
/**
|
||||
* @public
|
||||
|
||||
@@ -16,23 +16,16 @@
|
||||
<script lang="ts">
|
||||
import { Label, ListView, resizeObserver } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import presentation, { getClient, ObjectSearchCategory } from '@hcengineering/presentation'
|
||||
import presentation, { getClient, type ObjectSearchCategory } from '@hcengineering/presentation'
|
||||
|
||||
import { Class, Ref, Doc, SearchResultDoc } from '@hcengineering/core'
|
||||
|
||||
import { type SearchItem, packSearchResultsForListView, doFulltextSearch } from '../search'
|
||||
|
||||
import MentionResult from './MentionResult.svelte'
|
||||
|
||||
export let query: string = ''
|
||||
|
||||
interface SearchSection {
|
||||
category: ObjectSearchCategory
|
||||
items: SearchResultDoc[]
|
||||
}
|
||||
interface SearchItem {
|
||||
num: number
|
||||
item: SearchResultDoc
|
||||
category: ObjectSearchCategory
|
||||
}
|
||||
|
||||
let items: SearchItem[] = []
|
||||
let categories: ObjectSearchCategory[] = []
|
||||
|
||||
@@ -92,67 +85,6 @@
|
||||
return false
|
||||
}
|
||||
|
||||
function packSearchResultsForListView (sections: SearchSection[]): SearchItem[] {
|
||||
let results: SearchItem[] = []
|
||||
for (const section of sections) {
|
||||
const category = section.category
|
||||
const items = section.items
|
||||
|
||||
if (category.classToSearch !== undefined) {
|
||||
results = results.concat(
|
||||
items.map((item, num) => {
|
||||
return { num, category, item }
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
function findCategoryByClass (
|
||||
categories: ObjectSearchCategory[],
|
||||
_class: Ref<Class<Doc>>
|
||||
): ObjectSearchCategory | undefined {
|
||||
for (const category of categories) {
|
||||
if (category.classToSearch === _class) {
|
||||
return category
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
async function doFulltextSearch (classes: Array<Ref<Class<Doc>>>, query: string): Promise<SearchSection[]> {
|
||||
const result = await client.searchFulltext(
|
||||
{
|
||||
query: `${query}*`,
|
||||
classes
|
||||
},
|
||||
{
|
||||
limit: 10
|
||||
}
|
||||
)
|
||||
|
||||
const itemsByClass = new Map<Ref<Class<Doc>>, SearchResultDoc[]>()
|
||||
for (const item of result.docs) {
|
||||
const list = itemsByClass.get(item.doc._class)
|
||||
if (list === undefined) {
|
||||
itemsByClass.set(item.doc._class, [item])
|
||||
} else {
|
||||
list.push(item)
|
||||
}
|
||||
}
|
||||
|
||||
const sections: SearchSection[] = []
|
||||
for (const [_class, items] of itemsByClass.entries()) {
|
||||
const category = findCategoryByClass(categories, _class)
|
||||
if (category !== undefined) {
|
||||
sections.push({ category, items })
|
||||
}
|
||||
}
|
||||
|
||||
return sections
|
||||
}
|
||||
|
||||
async function updateItems (query: string): Promise<void> {
|
||||
const classesToSearch: Array<Ref<Class<Doc>>> = []
|
||||
for (const cat of categories) {
|
||||
@@ -161,7 +93,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
const sections = await doFulltextSearch(classesToSearch, query)
|
||||
const sections = await doFulltextSearch(client, classesToSearch, query, categories)
|
||||
items = packSearchResultsForListView(sections)
|
||||
}
|
||||
$: void updateItems(query)
|
||||
@@ -191,7 +123,7 @@
|
||||
{@const doc = item.item}
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="ap-menuItem withComp"
|
||||
class="ap-menuItem withComp h-8"
|
||||
on:click={() => {
|
||||
dispatchItem(doc)
|
||||
}}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
$: icon = value.icon !== undefined ? value.icon : undefined
|
||||
</script>
|
||||
|
||||
<div class="flex-row-center h-8">
|
||||
<div class="flex-row-center">
|
||||
<div class="flex-center p-1 content-dark-color flex-no-shrink">
|
||||
{#if icon !== undefined}
|
||||
<Icon {icon} size={'medium'} />
|
||||
|
||||
@@ -30,10 +30,12 @@ export { default as TextEditor } from './components/TextEditor.svelte'
|
||||
export { default as TextEditorStyleToolbar } from './components/TextEditorStyleToolbar.svelte'
|
||||
export { default as AttachIcon } from './components/icons/Attach.svelte'
|
||||
export { default as TableOfContents } from './components/toc/TableOfContents.svelte'
|
||||
export { default as MentionResult } from './components/MentionResult.svelte'
|
||||
export * from './components/node-view'
|
||||
export { default } from './plugin'
|
||||
export * from './types'
|
||||
export * from './utils'
|
||||
export * from './search'
|
||||
|
||||
export { FocusExtension, type FocusOptions, type FocusStorage } from './components/extension/focus'
|
||||
export { HeadingsExtension, type HeadingsOptions, type HeadingsStorage } from './components/extension/headings'
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
//
|
||||
// Copyright © 2023 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import type { Class, Ref, Doc, SearchResultDoc, TxOperations } from '@hcengineering/core'
|
||||
import { type ObjectSearchCategory } from '@hcengineering/presentation'
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface SearchSection {
|
||||
category: ObjectSearchCategory
|
||||
items: SearchResultDoc[]
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface SearchItem {
|
||||
num: number
|
||||
item: SearchResultDoc
|
||||
category: ObjectSearchCategory
|
||||
}
|
||||
|
||||
export function findCategoryByClass (
|
||||
categories: ObjectSearchCategory[],
|
||||
_class: Ref<Class<Doc>>
|
||||
): ObjectSearchCategory | undefined {
|
||||
for (const category of categories) {
|
||||
if (category.classToSearch === _class) {
|
||||
return category
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function packSearchResultsForListView (sections: SearchSection[]): SearchItem[] {
|
||||
let results: SearchItem[] = []
|
||||
for (const section of sections) {
|
||||
const category = section.category
|
||||
const items = section.items
|
||||
|
||||
if (category.classToSearch !== undefined) {
|
||||
results = results.concat(
|
||||
items.map((item, num) => {
|
||||
return { num, category, item }
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
export async function doFulltextSearch (
|
||||
client: TxOperations,
|
||||
classes: Array<Ref<Class<Doc>>>,
|
||||
query: string,
|
||||
categories: ObjectSearchCategory[]
|
||||
): Promise<SearchSection[]> {
|
||||
const result = await client.searchFulltext(
|
||||
{
|
||||
query: `${query}*`,
|
||||
classes
|
||||
},
|
||||
{
|
||||
limit: 10
|
||||
}
|
||||
)
|
||||
|
||||
const itemsByClass = new Map<Ref<Class<Doc>>, SearchResultDoc[]>()
|
||||
for (const item of result.docs) {
|
||||
const list = itemsByClass.get(item.doc._class)
|
||||
if (list === undefined) {
|
||||
itemsByClass.set(item.doc._class, [item])
|
||||
} else {
|
||||
list.push(item)
|
||||
}
|
||||
}
|
||||
|
||||
const sections: SearchSection[] = []
|
||||
for (const [_class, items] of itemsByClass.entries()) {
|
||||
const category = findCategoryByClass(categories, _class)
|
||||
if (category !== undefined) {
|
||||
sections.push({ category, items })
|
||||
}
|
||||
}
|
||||
|
||||
return sections
|
||||
}
|
||||
@@ -36,11 +36,11 @@
|
||||
"Assigned": "Assigned",
|
||||
"Created": "Created",
|
||||
"Subscribed": "Subscribed",
|
||||
|
||||
|
||||
"ShowPreview": "Show document preview",
|
||||
"ShowActions": "Show actions popup",
|
||||
|
||||
"ActionPlaceholder": "type to filter...",
|
||||
"ActionPlaceholder": "Search or run a command...",
|
||||
"General": "General",
|
||||
"Navigation": "Navigation",
|
||||
"Editor": "Editor",
|
||||
@@ -111,6 +111,8 @@
|
||||
"ChooseIcon": "Choose icon",
|
||||
"IconColor": "Choose icon color",
|
||||
"IconCategory": "Icon",
|
||||
"EmojiCategory": "Emoji"
|
||||
"EmojiCategory": "Emoji",
|
||||
"Type": "Type",
|
||||
"ToViewCommands": "to view available commands"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"Open": "Открыть",
|
||||
"Assignees": "Исполнители",
|
||||
"Labels": "Метки",
|
||||
"ActionPlaceholder": "введите для фильтрации...",
|
||||
"ActionPlaceholder": "Поиск или команда...",
|
||||
"General": "Общее",
|
||||
"Navigation": "Навигация",
|
||||
"Editor": "Редактор",
|
||||
@@ -107,6 +107,9 @@
|
||||
"ChooseIcon": "Выбрать иконку",
|
||||
"IconColor": "Выберите цвет",
|
||||
"IconCategory": "Иконка",
|
||||
"EmojiCategory": "Эмодзи"
|
||||
"EmojiCategory": "Эмодзи",
|
||||
|
||||
"Type": "Введите",
|
||||
"ToViewCommands": "чтобы увидеть команды"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,9 +13,14 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { WithLookup, Doc, Ref } from '@hcengineering/core'
|
||||
import { WithLookup, Doc, Ref, Class, SearchResultDoc } from '@hcengineering/core'
|
||||
import { getResource, translate } from '@hcengineering/platform'
|
||||
import { createQuery, getClient, ActionContext } from '@hcengineering/presentation'
|
||||
import presentation, {
|
||||
createQuery,
|
||||
getClient,
|
||||
ActionContext,
|
||||
ObjectSearchCategory
|
||||
} from '@hcengineering/presentation'
|
||||
import ui, {
|
||||
Button,
|
||||
closePopup,
|
||||
@@ -23,27 +28,35 @@
|
||||
Icon,
|
||||
IconArrowLeft,
|
||||
Label,
|
||||
EditWithIcon,
|
||||
IconSearch,
|
||||
deviceOptionsStore,
|
||||
capitalizeFirstLetter,
|
||||
formatKey,
|
||||
themeStore
|
||||
} from '@hcengineering/ui'
|
||||
import { Action, ViewContext } from '@hcengineering/view'
|
||||
import { Action, ActionCategory, ViewContext } from '@hcengineering/view'
|
||||
import { filterActions, getSelection } from '../actions'
|
||||
import view from '../plugin'
|
||||
import { focusStore, selectionStore } from '../selection'
|
||||
import { openDoc } from '../utils'
|
||||
import { ListView, resizeObserver } from '@hcengineering/ui'
|
||||
import ObjectPresenter from './ObjectPresenter.svelte'
|
||||
import { tick } from 'svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { createEventDispatcher, tick } from 'svelte'
|
||||
|
||||
import {
|
||||
type SearchSection,
|
||||
type SearchItem,
|
||||
packSearchResultsForListView,
|
||||
doFulltextSearch,
|
||||
MentionResult
|
||||
} from '@hcengineering/text-editor'
|
||||
|
||||
import ChevronDown from './icons/ChevronDown.svelte'
|
||||
import ChevronUp from './icons/ChevronUp.svelte'
|
||||
|
||||
export let viewContext: ViewContext
|
||||
|
||||
let search: string = ''
|
||||
let actions: Array<WithLookup<Action>> = []
|
||||
let input: EditWithIcon
|
||||
|
||||
const query = createQuery()
|
||||
|
||||
@@ -66,11 +79,14 @@
|
||||
let supportedActions: Array<WithLookup<Action>> = []
|
||||
let filteredActions: Array<WithLookup<Action>> = []
|
||||
|
||||
async function filterVisibleActions (actions: Array<WithLookup<Action>>, docs: Doc[]) {
|
||||
async function filterVisibleActions (
|
||||
actions: Array<WithLookup<Action>>,
|
||||
docs: Doc[]
|
||||
): Promise<Array<WithLookup<Action>>> {
|
||||
const resultActions: Array<WithLookup<Action>> = []
|
||||
|
||||
for (const action of actions) {
|
||||
if (!action.visibilityTester) {
|
||||
if (action.visibilityTester === undefined) {
|
||||
resultActions.push(action)
|
||||
} else {
|
||||
const visibilityTester = await getResource(action.visibilityTester)
|
||||
@@ -85,7 +101,7 @@
|
||||
|
||||
const client = getClient()
|
||||
|
||||
async function getSupportedActions (actions: Array<WithLookup<Action>>) {
|
||||
async function getSupportedActions (actions: Array<WithLookup<Action>>): Promise<void> {
|
||||
const docs = getSelection($focusStore, $selectionStore)
|
||||
let fActions: Array<WithLookup<Action>> = actions
|
||||
|
||||
@@ -114,15 +130,18 @@
|
||||
supportedActions = fActions.sort((a, b) => a.category.localeCompare(b.category))
|
||||
}
|
||||
|
||||
$: getSupportedActions(actions)
|
||||
$: void getSupportedActions(actions)
|
||||
|
||||
async function filterSearchActions (actions: Array<WithLookup<Action>>, search: string): Promise<void> {
|
||||
const res: Array<WithLookup<Action>> = []
|
||||
search = search.trim().toLowerCase()
|
||||
if (search.length > 0) {
|
||||
let preparedSearch = search.trim().toLowerCase()
|
||||
if (preparedSearch.charAt(0) === '/') {
|
||||
preparedSearch = preparedSearch.substring(1)
|
||||
}
|
||||
if (preparedSearch.length > 0) {
|
||||
for (const a of actions) {
|
||||
const tr = await translate(a.label, {}, $themeStore.language)
|
||||
if (tr.toLowerCase().includes(search)) {
|
||||
if (tr.toLowerCase().includes(preparedSearch)) {
|
||||
res.push(a)
|
||||
}
|
||||
}
|
||||
@@ -131,7 +150,7 @@
|
||||
filteredActions = actions
|
||||
}
|
||||
}
|
||||
$: filterSearchActions(supportedActions, search)
|
||||
$: void filterSearchActions(supportedActions, search)
|
||||
|
||||
let selection = 0
|
||||
let list: ListView
|
||||
@@ -140,24 +159,35 @@
|
||||
let activeAction: Action | undefined
|
||||
|
||||
async function handleSelection (evt: Event, selection: number): Promise<void> {
|
||||
const action = filteredActions[selection]
|
||||
if (action.actionPopup !== undefined) {
|
||||
activeAction = action
|
||||
return
|
||||
}
|
||||
const docs = getSelection($focusStore, $selectionStore)
|
||||
if (action.input === 'focus') {
|
||||
const impl = await getResource(action.action)
|
||||
if (impl !== undefined) {
|
||||
closePopup()
|
||||
impl(docs[0], evt, { ...action.actionProps, action })
|
||||
const item = items[selection]
|
||||
|
||||
if (item.item !== undefined) {
|
||||
const doc = item.item.doc
|
||||
void client.findOne(doc._class, { _id: doc._id }).then((value) => {
|
||||
if (value !== undefined) {
|
||||
void openDoc(client.getHierarchy(), value)
|
||||
}
|
||||
})
|
||||
} else if (item.action !== undefined) {
|
||||
const action = item.action
|
||||
if (action.actionPopup !== undefined) {
|
||||
activeAction = action
|
||||
return
|
||||
}
|
||||
}
|
||||
if (action.input === 'selection' || action.input === 'any' || action.input === 'none') {
|
||||
const impl = await getResource(action.action)
|
||||
if (impl !== undefined) {
|
||||
closePopup()
|
||||
impl(docs, evt, { ...action.actionProps, action })
|
||||
const docs = getSelection($focusStore, $selectionStore)
|
||||
if (action.input === 'focus') {
|
||||
const impl = await getResource(action.action)
|
||||
if (impl !== undefined) {
|
||||
closePopup()
|
||||
void impl(docs[0], evt, { ...action.actionProps, action })
|
||||
}
|
||||
}
|
||||
if (action.input === 'selection' || action.input === 'any' || action.input === 'none') {
|
||||
const impl = await getResource(action.action)
|
||||
if (impl !== undefined) {
|
||||
closePopup()
|
||||
void impl(docs, evt, { ...action.actionProps, action })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -176,11 +206,87 @@
|
||||
if (key.code === 'Enter') {
|
||||
key.preventDefault()
|
||||
key.stopPropagation()
|
||||
handleSelection(key, selection)
|
||||
void handleSelection(key, selection)
|
||||
}
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
interface SearchActionItem {
|
||||
num: number
|
||||
item?: SearchResultDoc
|
||||
category?: ObjectSearchCategory
|
||||
action?: WithLookup<Action>
|
||||
actionCategory?: ActionCategory | WithLookup<ActionCategory>
|
||||
}
|
||||
|
||||
function packSearchAndActions (
|
||||
searchItems: SearchItem[],
|
||||
filteredActions: Array<WithLookup<Action>>
|
||||
): SearchActionItem[] {
|
||||
let iter = -1
|
||||
const mappedActions: SearchActionItem[] = filteredActions.map((action, num: number) => {
|
||||
if (num > 0 && filteredActions[num - 1].$lookup?.category?.label !== action.$lookup?.category?.label) {
|
||||
iter = 0
|
||||
} else {
|
||||
iter++
|
||||
}
|
||||
return {
|
||||
num: iter,
|
||||
action,
|
||||
actionCategory: action.$lookup?.category
|
||||
}
|
||||
})
|
||||
return ([] as SearchActionItem[]).concat(searchItems).concat(mappedActions)
|
||||
}
|
||||
|
||||
let items: SearchActionItem[] = []
|
||||
let categories: ObjectSearchCategory[] = []
|
||||
|
||||
client
|
||||
.findAll(presentation.class.ObjectSearchCategory, { context: 'spotlight' })
|
||||
.then(async (results) => {
|
||||
categories = results
|
||||
await updateItems(search, filteredActions)
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e)
|
||||
})
|
||||
|
||||
async function updateItems (query: string, filteredActions: Array<WithLookup<Action>>): Promise<void> {
|
||||
const classesToSearch: Array<Ref<Class<Doc>>> = []
|
||||
for (const cat of categories) {
|
||||
if (cat.classToSearch !== undefined) {
|
||||
classesToSearch.push(cat.classToSearch)
|
||||
}
|
||||
}
|
||||
|
||||
let sections: SearchSection[] = []
|
||||
if (query !== '' && query.indexOf('/') !== 0) {
|
||||
sections = await doFulltextSearch(client, classesToSearch, query, categories)
|
||||
}
|
||||
|
||||
const searchItems = packSearchResultsForListView(sections)
|
||||
items = packSearchAndActions(searchItems, filteredActions)
|
||||
}
|
||||
|
||||
$: void updateItems(search, filteredActions)
|
||||
|
||||
let textHTML: HTMLInputElement
|
||||
let phTraslate: string = ''
|
||||
let autoFocus = !$deviceOptionsStore.isMobile
|
||||
|
||||
export function focus (): void {
|
||||
textHTML.focus()
|
||||
autoFocus = false
|
||||
}
|
||||
$: if (textHTML !== undefined) {
|
||||
if (autoFocus) focus()
|
||||
}
|
||||
|
||||
$: void translate(view.string.ActionPlaceholder, {}).then((res) => {
|
||||
phTraslate = res
|
||||
})
|
||||
</script>
|
||||
|
||||
<ActionContext
|
||||
@@ -191,13 +297,13 @@
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="selectPopup width-40"
|
||||
class="selectPopup width-40 actionsPopup"
|
||||
style:width="15rem"
|
||||
on:keydown={onKeydown}
|
||||
use:resizeObserver={() => dispatch('changeContent')}
|
||||
>
|
||||
{#if $selectionStore.docs.length > 0 || $focusStore.focus !== undefined || activeAction?.actionPopup !== undefined}
|
||||
<div class="mt-2 ml-2 flex-between flex-no-shrink">
|
||||
<div class="actionsDoc flex-between flex-no-shrink">
|
||||
{#if $selectionStore.docs.length > 0}
|
||||
<div class="item-box">
|
||||
<Label label={view.string.NumberItems} params={{ count: $selectionStore.docs.length }} />
|
||||
@@ -241,82 +347,189 @@
|
||||
on:close={async () => {
|
||||
activeAction = undefined
|
||||
await tick()
|
||||
input?.focus()
|
||||
textHTML?.focus()
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<div class="header">
|
||||
<EditWithIcon
|
||||
bind:this={input}
|
||||
icon={IconSearch}
|
||||
size={'large'}
|
||||
width={'100%'}
|
||||
autoFocus={!$deviceOptionsStore.isMobile}
|
||||
<div class="header actionsHeader">
|
||||
<input
|
||||
class="actionsInput"
|
||||
bind:this={textHTML}
|
||||
type="text"
|
||||
bind:value={search}
|
||||
placeholder={view.string.ActionPlaceholder}
|
||||
placeholder={phTraslate}
|
||||
on:change
|
||||
on:input
|
||||
on:keydown
|
||||
/>
|
||||
</div>
|
||||
<div class="scroll">
|
||||
<div class="box">
|
||||
<ListView
|
||||
bind:this={list}
|
||||
count={filteredActions.length}
|
||||
count={items.length}
|
||||
bind:selection
|
||||
on:click={async (evt) => {
|
||||
await handleSelection(evt, evt.detail)
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="category" let:item>
|
||||
{@const action = filteredActions[item]}
|
||||
{#if item === 0 || (item > 0 && filteredActions[item - 1].$lookup?.category?.label !== action.$lookup?.category?.label)}
|
||||
<!--Category for first item-->
|
||||
{#if action.$lookup?.category}
|
||||
<div class="category-box">
|
||||
<Label label={action.$lookup.category.label} />
|
||||
<svelte:fragment slot="category" let:item={num}>
|
||||
{@const item = items[num]}
|
||||
{#if item.num === 0}
|
||||
{#if item.category !== undefined}
|
||||
<div class="actionsCategory">
|
||||
<Label label={item.category.title} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if item.actionCategory}
|
||||
<div class="actionsCategory">
|
||||
<Label label={item.actionCategory.label} />
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="item" let:item>
|
||||
{@const action = filteredActions[item]}
|
||||
<div class="flex-row-center flex-between flex-grow ml-2 p-3 text-base cursor-pointer">
|
||||
<div class="mr-4 {selection === item ? 'caption-color' : 'content-dark-color'}">
|
||||
<Icon icon={action.icon ?? IconArrowLeft} size={'small'} />
|
||||
<svelte:fragment slot="item" let:item={num}>
|
||||
{@const item = items[num]}
|
||||
{#if item.item !== undefined}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div class="ap-menuItem withComp actionsSearchItem">
|
||||
<MentionResult value={item.item} />
|
||||
</div>
|
||||
<div class="flex-grow {selection === item ? 'caption-color' : 'content-color'}">
|
||||
<Label label={action.label} />
|
||||
</div>
|
||||
<div class="mr-2 text-md flex-row-center">
|
||||
{#if action.keyBinding}
|
||||
{#each action.keyBinding as key, i}
|
||||
{#if i !== 0}
|
||||
<div class="ml-2 mr-2 lower"><Label label={view.string.Or} /></div>
|
||||
{/if}
|
||||
<div class="flex-row-center">
|
||||
{#each formatKey(key) as k, jj}
|
||||
{#if jj !== 0}
|
||||
<div class="ml-1 mr-1 lower"><Label label={view.string.Then} /></div>
|
||||
{/if}
|
||||
{#each k as kk, j}
|
||||
<div class="flex-center text-sm key-box">
|
||||
{capitalizeFirstLetter(kk.trim())}
|
||||
</div>
|
||||
{/if}
|
||||
{#if item.action !== undefined}
|
||||
{@const action = item.action}
|
||||
<div class="flex-row-center flex-between flex-grow ml-2 text-base cursor-pointer actionsitem">
|
||||
<div class="mr-4 {selection === num ? 'caption-color' : 'content-dark-color'}">
|
||||
<Icon icon={action.icon ?? IconArrowLeft} size={'small'} />
|
||||
</div>
|
||||
<div class="flex-grow {selection === num ? 'caption-color' : 'content-color'}">
|
||||
<Label label={action.label} />
|
||||
</div>
|
||||
<div class="mr-2 text-md flex-row-center">
|
||||
{#if action.keyBinding}
|
||||
{#each action.keyBinding as key, i}
|
||||
{#if i !== 0}
|
||||
<div class="ml-2 mr-2 lower"><Label label={view.string.Or} /></div>
|
||||
{/if}
|
||||
<div class="flex-row-center">
|
||||
{#each formatKey(key) as k, jj}
|
||||
{#if jj !== 0}
|
||||
<div class="ml-1 mr-1 lower"><Label label={view.string.Then} /></div>
|
||||
{/if}
|
||||
{#each k as kk, j}
|
||||
<div class="flex-center text-sm key-box">
|
||||
{capitalizeFirstLetter(kk.trim())}
|
||||
</div>
|
||||
{/each}
|
||||
{/each}
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</ListView>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actionsHint">
|
||||
<div class="actionsHintLable">
|
||||
<span class="hintNav">
|
||||
<ChevronUp size={'small'} />
|
||||
</span>
|
||||
<span class="hintNav">
|
||||
<ChevronDown size={'small'} />
|
||||
</span>
|
||||
<span class="ml mr">
|
||||
<Label label={view.string.Type} />
|
||||
</span><span class="hintNav">/</span>
|
||||
<span class="ml"><Label label={view.string.ToViewCommands} /></span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="menu-space" />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.actionsHint {
|
||||
border-top: 1px solid var(--theme-popup-divider);
|
||||
padding: 0 1.25rem;
|
||||
font-size: 0.8125rem;
|
||||
|
||||
.actionsHintLable {
|
||||
height: 2.75rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.hintNav {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
margin: 0.13rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.ml {
|
||||
margin-left: 0.62rem;
|
||||
}
|
||||
.mr {
|
||||
margin-right: 0.62rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.actionsDoc {
|
||||
margin-top: 1rem;
|
||||
margin-left: 1.25rem;
|
||||
margin-right: 1.25rem;
|
||||
}
|
||||
|
||||
.actionsInput {
|
||||
width: 100%;
|
||||
caret-color: var(--theme-caret-color);
|
||||
border: none;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 1.125rem;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--theme-dark-color);
|
||||
}
|
||||
}
|
||||
|
||||
.header.actionsHeader {
|
||||
padding-top: 1rem;
|
||||
padding-left: 1.25rem;
|
||||
padding-right: 1.25rem;
|
||||
}
|
||||
|
||||
.selectPopup.actionsPopup {
|
||||
max-height: 30rem;
|
||||
|
||||
.actionsCategory {
|
||||
padding: 0.5rem 1.25rem;
|
||||
font-size: 0.625rem;
|
||||
letter-spacing: 0.0625rem;
|
||||
color: var(--theme-dark-color);
|
||||
text-transform: uppercase;
|
||||
line-height: 1rem;
|
||||
}
|
||||
|
||||
.actionsSearchItem {
|
||||
height: 2.25rem;
|
||||
display: flex;
|
||||
margin: 0.25rem;
|
||||
}
|
||||
|
||||
.actionsitem {
|
||||
font-size: 0.875rem;
|
||||
padding: 0.375rem;
|
||||
margin: 0.25rem 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.key-box {
|
||||
padding: 0 0.5rem;
|
||||
min-width: 1.5rem;
|
||||
@@ -325,9 +538,11 @@
|
||||
border: 1px solid var(--theme-button-border);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.key-box + .key-box {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.item-box {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.5rem;
|
||||
@@ -335,14 +550,4 @@
|
||||
background-color: var(--theme-divider-color);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
.category-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.5rem 1rem;
|
||||
min-height: 2rem;
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
font-size: 0.625rem;
|
||||
color: var(--theme-caption-color);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<!--
|
||||
// Copyright © 2023 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
export let size: 'small' | 'medium' | 'large'
|
||||
const fill: string = 'currentColor'
|
||||
</script>
|
||||
|
||||
<svg class="svg-{size}" {fill} viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 8.24999L2.25 4.49999L2.775 3.97499L6 7.19999L9.225 3.97499L9.75 4.49999L6 8.24999Z" fill-opacity="0.6" />
|
||||
</svg>
|
||||
@@ -0,0 +1,22 @@
|
||||
<!--
|
||||
// Copyright © 2023 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
export let size: 'small' | 'medium' | 'large'
|
||||
const fill: string = 'currentColor'
|
||||
</script>
|
||||
|
||||
<svg class="svg-{size}" {fill} viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 3.75L2.25 7.5L2.78033 8.03033L6 4.81066L9.21967 8.03033L9.75 7.5L6 3.75Z" fill-opacity="0.6" />
|
||||
</svg>
|
||||
@@ -98,7 +98,9 @@ export default mergeIds(viewId, view, {
|
||||
IconColor: '' as IntlString,
|
||||
IconCategory: '' as IntlString,
|
||||
EmojiCategory: '' as IntlString,
|
||||
NumberItems: '' as IntlString
|
||||
NumberItems: '' as IntlString,
|
||||
Type: '' as IntlString,
|
||||
ToViewCommands: '' as IntlString
|
||||
},
|
||||
function: {
|
||||
CreateDocMiddleware: '' as Resource<PresentationMiddlewareCreator>
|
||||
|
||||
@@ -21,7 +21,7 @@ test.describe('actions tests', () => {
|
||||
await page.click('td:has-text("Frontend Engineer")')
|
||||
|
||||
await page.press('body', 'Meta+k')
|
||||
await page.fill('[placeholder="type\\ to\\ filter\\.\\.\\."]', 'Talent')
|
||||
await page.fill('[placeholder="Search\\ or\\ run\\ a\\ command\\.\\.\\."]', 'Talent')
|
||||
expect(await page.locator('div.selectPopup :text("New Talent")').count()).toBe(1)
|
||||
await page.click('div.selectPopup :text("New Talent")')
|
||||
await page.click('button#card-close')
|
||||
@@ -34,7 +34,7 @@ test.describe('actions tests', () => {
|
||||
await expect(page).toHaveURL(`${PlatformURI}/workbench/sanity-ws/recruit/talents`)
|
||||
|
||||
await page.press('body', 'Meta+k')
|
||||
await page.fill('[placeholder="type\\ to\\ filter\\.\\.\\."]', 'go to')
|
||||
await page.fill('[placeholder="Search\\ or\\ run\\ a\\ command\\.\\.\\."]', 'go to')
|
||||
expect(await page.locator('div.selectPopup :text("Go To Vacancies")').count()).toBe(1)
|
||||
await page.click('div.selectPopup :text("Go To Vacancies")')
|
||||
|
||||
@@ -48,7 +48,7 @@ test.describe('actions tests', () => {
|
||||
await expect(page).toHaveURL(`${PlatformURI}/workbench/sanity-ws/recruit/talents`)
|
||||
|
||||
await page.press('body', 'Meta+k')
|
||||
await page.fill('[placeholder="type\\ to\\ filter\\.\\.\\."]', 'go to')
|
||||
await page.fill('[placeholder="Search\\ or\\ run\\ a\\ command\\.\\.\\."]', 'go to')
|
||||
expect(await page.locator('div.selectPopup :text("Go To Applications")').count()).toBe(1)
|
||||
await page.click('div.selectPopup :text("Go To Applications")')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user