diff --git a/models/task/src/index.ts b/models/task/src/index.ts index dbe770f162..65a339f495 100644 --- a/models/task/src/index.ts +++ b/models/task/src/index.ts @@ -64,6 +64,7 @@ import { getEmbeddedLabel, type Asset, type IntlString } from '@hcengineering/pl import setting from '@hcengineering/setting' import tags from '@hcengineering/tags' import { + type TaskStatusFactory, calculateStatuses, findStatusAttr, type KanbanCard, @@ -489,7 +490,7 @@ export function createModel (builder: Builder): void { ofAttribute: task.attribute.State, label: task.string.StateBacklog, icon: task.icon.TaskState, - color: PaletteColorIndexes.Coin, + color: PaletteColorIndexes.Cloud, defaultStatusName: 'Backlog', order: 0 }, @@ -503,7 +504,7 @@ export function createModel (builder: Builder): void { ofAttribute: task.attribute.State, label: task.string.StateActive, icon: task.icon.TaskState, - color: PaletteColorIndexes.Blueberry, + color: PaletteColorIndexes.Porpoise, defaultStatusName: 'New state', order: 0 }, @@ -517,7 +518,7 @@ export function createModel (builder: Builder): void { ofAttribute: task.attribute.State, label: task.string.DoneStatesWon, icon: task.icon.TaskState, - color: PaletteColorIndexes.Houseplant, + color: PaletteColorIndexes.Grass, defaultStatusName: 'Won', order: 0 }, @@ -531,7 +532,7 @@ export function createModel (builder: Builder): void { ofAttribute: task.attribute.State, label: task.string.DoneStatesLost, icon: task.icon.TaskState, - color: PaletteColorIndexes.Firework, + color: PaletteColorIndexes.Coin, defaultStatusName: 'Lost', order: 0 }, @@ -602,7 +603,10 @@ export function createModel (builder: Builder): void { /** * @public */ -export type FixTaskData = Omit, 'space' | 'statuses' | 'parent'> & { _id?: TaskType['_id'] } +export type FixTaskData = Omit, 'space' | 'statuses' | 'statusCategories' | 'parent'> & { + _id?: TaskType['_id'] + statusCategories: TaskType['statusCategories'] | TaskStatusFactory[] +} export interface FixTaskResult { taskTypes: TaskType[] projectTypes: ProjectType[] @@ -694,35 +698,74 @@ export async function fixTaskTypes ( const statusAttr = findStatusAttr(client.hierarchy, data.ofClass) // Ensure we have at leas't one item in every category. for (const c of data.statusCategories) { - const cat = await client.model.findOne(core.class.StatusCategory, { _id: c }) - const st = statuses.find((it) => it.category === c) - if (st === undefined) { - // We need to add new status into missing category - const statusId: Ref = generateId() - await client.create(DOMAIN_STATUS, { - _id: statusId, - _class: data.statusClass, - category: c, - modifiedBy: core.account.ConfigUser, - modifiedOn: Date.now(), - name: cat?.defaultStatusName ?? 'New state', - space: task.space.Statuses, - ofAttribute: statusAttr._id - }) - dStatuses.push(statusId) + const category = typeof c === 'string' ? c : c.category + const cat = await client.model.findOne(core.class.StatusCategory, { _id: category }) - await client.update( - DOMAIN_SPACE, - { - _id: t._id - }, - { $push: { statuses: { _id: statusId } } } - ) - t.statuses.push({ _id: statusId, taskType: taskTypeId }) + const st = statuses.find((it) => it.category === category) + const newStatuses: Ref[] = [] + if (st === undefined) { + if (typeof c === 'string') { + // We need to add new status into missing category + const statusId: Ref = generateId() + await client.create(DOMAIN_STATUS, { + _id: statusId, + _class: data.statusClass, + category, + modifiedBy: core.account.ConfigUser, + modifiedOn: Date.now(), + name: cat?.defaultStatusName ?? 'New state', + space: task.space.Statuses, + ofAttribute: statusAttr._id + }) + newStatuses.push(statusId) + dStatuses.push(statusId) + + await client.update( + DOMAIN_SPACE, + { + _id: t._id + }, + { $push: { statuses: newStatuses.map((it) => ({ _id: it })) } } + ) + t.statuses.push(...newStatuses.map((it) => ({ _id: it, taskType: taskTypeId }))) + } else { + for (const sts of c.statuses) { + const stsName = Array.isArray(sts) ? sts[0] : sts + const color = Array.isArray(sts) ? sts[1] : undefined + const st = statuses.find((it) => it.name.toLowerCase() === stsName.toLowerCase()) + if (st === undefined) { + // We need to add new status into missing category + const statusId: Ref = generateId() + await client.create(DOMAIN_STATUS, { + _id: statusId, + _class: data.statusClass, + category, + modifiedBy: core.account.ConfigUser, + modifiedOn: Date.now(), + name: stsName, + color, + space: task.space.Statuses, + ofAttribute: statusAttr._id + }) + newStatuses.push(statusId) + dStatuses.push(statusId) + } + + await client.update( + DOMAIN_SPACE, + { + _id: t._id + }, + { $push: { statuses: newStatuses.map((it) => ({ _id: it })) } } + ) + t.statuses.push(...newStatuses.map((it) => ({ _id: it, taskType: taskTypeId }))) + } + } } } const taskType: TaskType = { ...data, + statusCategories: data.statusCategories.map((it) => (typeof it === 'string' ? it : it.category)), parent: t._id, _id: taskTypeId, _class: task.class.TaskType, diff --git a/models/tracker/src/index.ts b/models/tracker/src/index.ts index ab96d2faca..7df44fa292 100644 --- a/models/tracker/src/index.ts +++ b/models/tracker/src/index.ts @@ -14,21 +14,20 @@ // import activity from '@hcengineering/activity' +import chunter from '@hcengineering/chunter' import { type Builder } from '@hcengineering/model' import core from '@hcengineering/model-core' +import { generateClassNotificationTypes } from '@hcengineering/model-notification' +import presentation from '@hcengineering/model-presentation' import task from '@hcengineering/model-task' import view from '@hcengineering/model-view' import workbench from '@hcengineering/model-workbench' import notification from '@hcengineering/notification' import setting from '@hcengineering/setting' import { trackerId } from '@hcengineering/tracker' -import { generateClassNotificationTypes } from '@hcengineering/model-notification' -import presentation from '@hcengineering/model-presentation' -import { PaletteColorIndexes } from '@hcengineering/ui/src/colors' -import chunter from '@hcengineering/chunter' -import tracker from './plugin' import { createActions as defineActions } from './actions' +import tracker from './plugin' import { definePresenters } from './presenters' import { TComponent, @@ -130,78 +129,6 @@ function defineNotifications (builder: Builder): void { ) } -function defineStatusCategories (builder: Builder): void { - builder.createDoc( - core.class.StatusCategory, - core.space.Model, - { - ofAttribute: tracker.attribute.IssueStatus, - label: tracker.string.CategoryBacklog, - icon: tracker.icon.CategoryBacklog, - color: PaletteColorIndexes.Cloud, - defaultStatusName: 'Backlog', - order: 0 - }, - tracker.issueStatusCategory.Backlog - ) - - builder.createDoc( - core.class.StatusCategory, - core.space.Model, - { - ofAttribute: tracker.attribute.IssueStatus, - label: tracker.string.CategoryUnstarted, - icon: tracker.icon.CategoryUnstarted, - color: PaletteColorIndexes.Porpoise, - defaultStatusName: 'Todo', - order: 1 - }, - tracker.issueStatusCategory.Unstarted - ) - - builder.createDoc( - core.class.StatusCategory, - core.space.Model, - { - ofAttribute: tracker.attribute.IssueStatus, - label: tracker.string.CategoryStarted, - icon: tracker.icon.CategoryStarted, - color: PaletteColorIndexes.Cerulean, - defaultStatusName: 'In Progress', - order: 2 - }, - tracker.issueStatusCategory.Started - ) - - builder.createDoc( - core.class.StatusCategory, - core.space.Model, - { - ofAttribute: tracker.attribute.IssueStatus, - label: tracker.string.CategoryCompleted, - icon: tracker.icon.CategoryCompleted, - color: PaletteColorIndexes.Grass, - defaultStatusName: 'Done', - order: 3 - }, - tracker.issueStatusCategory.Completed - ) - - builder.createDoc( - core.class.StatusCategory, - core.space.Model, - { - ofAttribute: tracker.attribute.IssueStatus, - label: tracker.string.CategoryCanceled, - icon: tracker.icon.CategoryCanceled, - color: PaletteColorIndexes.Coin, - defaultStatusName: 'Canceled', - order: 4 - }, - tracker.issueStatusCategory.Canceled - ) -} - /** * Define filters */ @@ -483,8 +410,6 @@ export function createModel (builder: Builder): void { defineViewlets(builder) - defineStatusCategories(builder) - const issuesId = 'issues' const componentsId = 'components' const milestonesId = 'milestones' diff --git a/models/tracker/src/migration.ts b/models/tracker/src/migration.ts index 3adb25162f..4a83c7fcc3 100644 --- a/models/tracker/src/migration.ts +++ b/models/tracker/src/migration.ts @@ -13,7 +13,15 @@ // limitations under the License. // -import core, { SortingOrder, TxOperations, generateId, type Data, type Ref, type Status } from '@hcengineering/core' +import core, { + DOMAIN_STATUS, + SortingOrder, + TxOperations, + generateId, + type Data, + type Ref, + type Status +} from '@hcengineering/core' import { createOrUpdate, tryMigrate, @@ -22,10 +30,15 @@ import { type MigrationUpgradeClient } from '@hcengineering/model' import { DOMAIN_SPACE } from '@hcengineering/model-core' -import { createProjectType, fixTaskTypes } from '@hcengineering/model-task' +import { DOMAIN_TASK, createProjectType, fixTaskTypes } from '@hcengineering/model-task' import tags from '@hcengineering/tags' -import task, { type TaskType } from '@hcengineering/task' -import { TimeReportDayType } from '@hcengineering/tracker' +import task, { type ProjectType, type TaskType } from '@hcengineering/task' +import { + TimeReportDayType, + baseIssueTaskStatuses, + classicIssueTaskStatuses, + createStatesData +} from '@hcengineering/tracker' import tracker from './plugin' async function createDefaultProject (tx: TxOperations): Promise { @@ -38,20 +51,7 @@ async function createDefaultProject (tx: TxOperations): Promise { }) if ((await tx.findOne(task.class.ProjectType, { _id: tracker.ids.ClassingProjectType })) === undefined) { - const categories = await tx.findAll( - core.class.StatusCategory, - { ofAttribute: tracker.attribute.IssueStatus }, - { sort: { order: SortingOrder.Ascending } } - ) - const states: Omit, 'rank'>[] = [] - - for (const category of categories) { - states.push({ - ofAttribute: tracker.attribute.IssueStatus, - name: category.defaultStatusName, - category: category._id - }) - } + const states: Omit, 'rank'>[] = createStatesData(classicIssueTaskStatuses) await createProjectType( tx, { @@ -68,7 +68,7 @@ async function createDefaultProject (tx: TxOperations): Promise { factory: states, ofClass: tracker.class.Issue, targetClass: tracker.class.Issue, - statusCategories: categories.map((it) => it._id), + statusCategories: classicIssueTaskStatuses.map((it) => it.category), statusClass: core.class.Status, kind: 'both', allowedAsChildOf: [tracker.taskTypes.Issue] @@ -80,26 +80,8 @@ async function createDefaultProject (tx: TxOperations): Promise { if ((await tx.findOne(task.class.ProjectType, { _id: tracker.ids.BaseProjectType })) === undefined) { const issueId: Ref = generateId() - const baseCategories = [ - task.statusCategory.UnStarted, - task.statusCategory.Active, - task.statusCategory.Won, - task.statusCategory.Lost - ] - const categories = await tx.findAll( - core.class.StatusCategory, - { _id: { $in: baseCategories } }, - { sort: { order: SortingOrder.Ascending } } - ) - const states: Omit, 'rank'>[] = [] - for (const category of categories) { - states.push({ - ofAttribute: tracker.attribute.IssueStatus, - name: category.defaultStatusName, - category: category._id - }) - } + const states: Omit, 'rank'>[] = createStatesData(baseIssueTaskStatuses) await createProjectType( tx, { @@ -116,7 +98,7 @@ async function createDefaultProject (tx: TxOperations): Promise { factory: states, ofClass: tracker.class.Issue, targetClass: tracker.class.Issue, - statusCategories: baseCategories, + statusCategories: baseIssueTaskStatuses.map((it) => it.category), statusClass: core.class.Status, kind: 'both', allowedAsChildOf: [issueId] @@ -183,13 +165,7 @@ async function fixTrackerTaskTypes (client: MigrationClient): Promise { descriptor: tracker.descriptors.Issue, ofClass: tracker.class.Issue, targetClass: tracker.class.Issue, - statusCategories: [ - tracker.issueStatusCategory.Backlog, - tracker.issueStatusCategory.Unstarted, - tracker.issueStatusCategory.Started, - tracker.issueStatusCategory.Completed, - tracker.issueStatusCategory.Canceled - ], + statusCategories: classicIssueTaskStatuses, statusClass: tracker.class.IssueStatus, kind: 'task', allowedAsChildOf: [typeId] @@ -214,6 +190,112 @@ export const trackerOperation: MigrateOperation = { ) } }, + { + state: 'migrate-category-types', + func: async (client) => { + // + await client.update( + DOMAIN_STATUS, + { _class: tracker.class.IssueStatus, category: tracker.issueStatusCategory.Backlog }, + { $set: { category: task.statusCategory.UnStarted } } + ) + + await client.update( + DOMAIN_STATUS, + { _class: tracker.class.IssueStatus, category: tracker.issueStatusCategory.Unstarted }, + { $set: { category: task.statusCategory.Active } } + ) + await client.update( + DOMAIN_STATUS, + { _class: tracker.class.IssueStatus, category: tracker.issueStatusCategory.Started }, + { $set: { category: task.statusCategory.Active } } + ) + + await client.update( + DOMAIN_STATUS, + { _class: tracker.class.IssueStatus, category: tracker.issueStatusCategory.Completed }, + { $set: { category: task.statusCategory.Won } } + ) + await client.update( + DOMAIN_STATUS, + { _class: tracker.class.IssueStatus, category: tracker.issueStatusCategory.Canceled }, + { $set: { category: task.statusCategory.Lost } } + ) + + // We need to update Project and TaskTypes. + const projectTypes = await client.find(DOMAIN_SPACE, { _class: task.class.ProjectType }) + + // We need to update Project and TaskTypes. + const taskTypes = await client.find(DOMAIN_TASK, { _class: task.class.TaskType }) + + const ptUpdate = new Map, ProjectType>() + const ttUpdate = new Map, TaskType>() + + for (const tt of taskTypes) { + if (tt.statusCategories.includes(tracker.issueStatusCategory.Backlog)) { + // We need to replace category + tt.statusCategories = [ + task.statusCategory.UnStarted, + task.statusCategory.Active, + task.statusCategory.Won, + task.statusCategory.Lost + ] + ttUpdate.set(tt._id, tt) + } + } + + // We need to fix duplicate statuses per category. + const toRemove: Ref[] = [] + for (const c of [ + task.statusCategory.UnStarted, + task.statusCategory.Active, + task.statusCategory.Won, + task.statusCategory.Lost + ]) { + const allStatuses = await client.find( + DOMAIN_STATUS, + { _class: tracker.class.IssueStatus, category: c }, + { projection: { name: 1, _id: 1 } } + ) + let idx = -1 + for (const s of allStatuses) { + idx++ + const sName = s.name.trim().toLowerCase() + const prev = allStatuses.findIndex((it) => it.name.trim().toLowerCase() === sName) + if (prev !== idx) { + const prevStatus = allStatuses[prev] + + // We have a duplicate tasks + await client.update(DOMAIN_TASK, { status: s._id }, { $set: { status: prevStatus._id } }) + + for (const tt of taskTypes) { + const pos = tt.statuses.indexOf(s._id) + if (pos !== -1) { + tt.statuses[pos] = prevStatus._id + ttUpdate.set(tt._id, tt) + } + } + + for (const pt of projectTypes) { + const pos = pt.statuses.findIndex((q) => q._id === s._id) + if (pos !== -1) { + pt.statuses[pos]._id = prevStatus._id + ptUpdate.set(pt._id, pt) + } + } + + toRemove.push(s._id) + } + } + } + for (const v of ptUpdate.values()) { + await client.update(DOMAIN_SPACE, { _id: v._id }, { $set: { statuses: v.statuses } }) + } + for (const v of ttUpdate.values()) { + await client.update(DOMAIN_TASK, { _id: v._id }, { $set: { statuses: v.statuses } }) + } + } + }, { state: 'fixTaskTypes', func: fixTrackerTaskTypes diff --git a/models/tracker/src/plugin.ts b/models/tracker/src/plugin.ts index 9ef276915b..f379eada1f 100644 --- a/models/tracker/src/plugin.ts +++ b/models/tracker/src/plugin.ts @@ -15,7 +15,7 @@ // import { type DocUpdateMessageViewlet, type TxViewlet } from '@hcengineering/activity' import { type ChatMessageViewlet } from '@hcengineering/chunter' -import { type Doc, type Ref } from '@hcengineering/core' +import { type StatusCategory, type Doc, type Ref } from '@hcengineering/core' import { type ObjectSearchCategory, type ObjectSearchFactory } from '@hcengineering/model-presentation' import { type NotificationGroup, type NotificationType } from '@hcengineering/notification' import { mergeIds, type IntlString, type Resource } from '@hcengineering/platform' @@ -109,5 +109,14 @@ export default mergeIds(trackerId, tracker, { DeleteProject: '' as Ref>>, DeleteProjectClean: '' as Ref>>, DeleteIssue: '' as Ref>> + }, + + // For migration only + issueStatusCategory: { + Backlog: '' as Ref, + Unstarted: '' as Ref, + Started: '' as Ref, + Completed: '' as Ref, + Canceled: '' as Ref } }) diff --git a/plugins/task-resources/src/components/state/StatePresenter.svelte b/plugins/task-resources/src/components/state/StatePresenter.svelte index 84ce64b035..cb3a556f15 100644 --- a/plugins/task-resources/src/components/state/StatePresenter.svelte +++ b/plugins/task-resources/src/components/state/StatePresenter.svelte @@ -82,13 +82,10 @@ dispatch('accent-color', color) } - $: color = - projectState?.color !== undefined - ? getPlatformColorDef( - projectState.color ?? category?.color ?? getColorNumberByText(value?.name ?? ''), - $themeStore.dark - ) - : undefined + $: color = getPlatformColorDef( + projectState?.color ?? category?.color ?? getColorNumberByText(value?.name ?? ''), + $themeStore.dark + ) $: dispatchAccentColor(color) onMount(() => { @@ -127,6 +124,7 @@
{ - if (state.category !== undefined && isTaskCategory(state.category)) { + if (state.category !== undefined) { selectIcon(elements[i + prevIndex], state) } else { onColor(state, color, elements[i + prevIndex]) diff --git a/plugins/task/src/index.ts b/plugins/task/src/index.ts index 43ab491f4e..199793bbdf 100644 --- a/plugins/task/src/index.ts +++ b/plugins/task/src/index.ts @@ -109,6 +109,14 @@ export interface TaskTypeDescriptor extends Doc { allowCreate: boolean } +/** + * @public + */ +export interface TaskStatusFactory { + category: Ref + statuses: (string | [string, number])[] +} + /** * @public */ diff --git a/plugins/task/src/utils.ts b/plugins/task/src/utils.ts index 41c18d9e11..a5bf2d6b51 100644 --- a/plugins/task/src/utils.ts +++ b/plugins/task/src/utils.ts @@ -23,7 +23,6 @@ import core, { IdMap, Ref, Status, - StatusCategory, TxOperations, generateId, type AnyAttribute, @@ -138,18 +137,6 @@ export async function createState ( return res } -/** - * @public - */ -export function isTaskCategory (category: Ref): boolean { - return ( - category === task.statusCategory.Active || - category === task.statusCategory.Active || - category === task.statusCategory.Won || - category === task.statusCategory.Lost - ) -} - /** * @public */ diff --git a/plugins/tracker-resources/src/components/icons/StatusIcon.svelte b/plugins/tracker-resources/src/components/icons/StatusIcon.svelte deleted file mode 100644 index 9b5a577d5a..0000000000 --- a/plugins/tracker-resources/src/components/icons/StatusIcon.svelte +++ /dev/null @@ -1,94 +0,0 @@ - - - - {#if category._id === tracker.issueStatusCategory.Backlog || category._id === task.statusCategory.UnStarted} - - {:else if category._id === tracker.issueStatusCategory.Unstarted} - - {:else if category._id === tracker.issueStatusCategory.Started} - - {#if statusIcon.count && statusIcon.index} - - {:else} - - {/if} - {:else if category._id === tracker.issueStatusCategory.Completed} - - - {:else if category._id === tracker.issueStatusCategory.Canceled} - - - {/if} - diff --git a/plugins/tracker-resources/src/components/issues/DueDateEditor.svelte b/plugins/tracker-resources/src/components/issues/DueDateEditor.svelte index 5ac64928f2..693b235486 100644 --- a/plugins/tracker-resources/src/components/issues/DueDateEditor.svelte +++ b/plugins/tracker-resources/src/components/issues/DueDateEditor.svelte @@ -13,18 +13,19 @@ // limitations under the License. --> -{#if category !== undefined && isTaskCategory(category._id)} - -{:else if icon !== undefined && color !== undefined && category !== undefined} - -{/if} + diff --git a/plugins/tracker-resources/src/components/issues/Issues.svelte b/plugins/tracker-resources/src/components/issues/Issues.svelte index 088162a780..8d04d5705b 100644 --- a/plugins/tracker-resources/src/components/issues/Issues.svelte +++ b/plugins/tracker-resources/src/components/issues/Issues.svelte @@ -25,6 +25,8 @@ import tracker from '../../plugin' import IssuesView from './IssuesView.svelte' + import task from '@hcengineering/task' + export let currentSpace: Ref | undefined = undefined export let baseQuery: DocumentQuery = {} export let title: IntlString @@ -57,13 +59,9 @@ let activeStatuses: Ref[] = [] - $: activeStatusQuery.query( - tracker.class.IssueStatus, - { category: { $in: [tracker.issueStatusCategory.Unstarted, tracker.issueStatusCategory.Started] } }, - (result) => { - activeStatuses = result.map(({ _id }) => _id) - } - ) + $: activeStatusQuery.query(tracker.class.IssueStatus, { category: task.statusCategory.Active }, (result) => { + activeStatuses = result.map(({ _id }) => _id) + }) let active: DocumentQuery $: active = { status: { $in: activeStatuses }, ...spaceQuery } @@ -72,13 +70,9 @@ let backlogStatuses: Ref[] = [] let backlog: DocumentQuery = {} - $: backlogStatusQuery.query( - tracker.class.IssueStatus, - { category: tracker.issueStatusCategory.Backlog }, - (result) => { - backlogStatuses = result.map(({ _id }) => _id) - } - ) + $: backlogStatusQuery.query(tracker.class.IssueStatus, { category: task.statusCategory.UnStarted }, (result) => { + backlogStatuses = result.map(({ _id }) => _id) + }) $: backlog = { status: { $in: backlogStatuses }, ...spaceQuery } $: queries = { all, active, backlog } diff --git a/plugins/tracker-resources/src/components/issues/StatusFilterValuePresenter.svelte b/plugins/tracker-resources/src/components/issues/StatusFilterValuePresenter.svelte index 0954939e19..296364fe69 100644 --- a/plugins/tracker-resources/src/components/issues/StatusFilterValuePresenter.svelte +++ b/plugins/tracker-resources/src/components/issues/StatusFilterValuePresenter.svelte @@ -54,7 +54,7 @@
{#each statuses as value, i (value._id)} - {#if value && i < 5} + {#if value != null && i < 5}
diff --git a/plugins/tracker-resources/src/components/issues/StatusRefPresenter.svelte b/plugins/tracker-resources/src/components/issues/StatusRefPresenter.svelte index dc8bbf574f..6391219c19 100644 --- a/plugins/tracker-resources/src/components/issues/StatusRefPresenter.svelte +++ b/plugins/tracker-resources/src/components/issues/StatusRefPresenter.svelte @@ -29,15 +29,13 @@ $: statusValue = value ? $statusStore.byId.get(value) : undefined -{#if value} - -{/if} + diff --git a/plugins/tracker-resources/src/components/issues/edit/SubIssueSelector.svelte b/plugins/tracker-resources/src/components/issues/edit/SubIssueSelector.svelte index ccfe0ba26a..dc43dd63bf 100644 --- a/plugins/tracker-resources/src/components/issues/edit/SubIssueSelector.svelte +++ b/plugins/tracker-resources/src/components/issues/edit/SubIssueSelector.svelte @@ -15,6 +15,7 @@