UBER-496: Fix few issues (#3439)

This commit is contained in:
Andrey Sobolev 2023-06-16 18:29:07 +07:00 committed by GitHub
parent 354e76e1f1
commit 05d273bb59
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 277 additions and 61 deletions

5
.vscode/launch.json vendored
View File

@ -69,7 +69,10 @@
"TRANSACTOR_URL": "ws:/localhost:3333", "TRANSACTOR_URL": "ws:/localhost:3333",
"ACCOUNT_PORT": "3000", "ACCOUNT_PORT": "3000",
"FRONT_URL": "http://localhost:8080", "FRONT_URL": "http://localhost:8080",
"SES_URL": "http://localhost:8091" "SES_URL": "http://localhost:8091",
"MINIO_ACCESS_KEY": "minioadmin",
"MINIO_SECRET_KEY": "minioadmin",
"MINIO_ENDPOINT": "localhost"
}, },
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"], "runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"sourceMaps": true, "sourceMaps": true,

View File

@ -627,6 +627,9 @@ export function createModel (builder: Builder): void {
builder.mixin(core.class.Account, core.class.Class, view.mixin.ObjectPresenter, { builder.mixin(core.class.Account, core.class.Class, view.mixin.ObjectPresenter, {
presenter: contact.component.EmployeeAccountPresenter presenter: contact.component.EmployeeAccountPresenter
}) })
builder.mixin(core.class.Account, core.class.Class, view.mixin.AttributePresenter, {
presenter: contact.component.EmployeeAccountRefPresenter
})
builder.mixin(contact.class.Organization, core.class.Class, view.mixin.ObjectPresenter, { builder.mixin(contact.class.Organization, core.class.Class, view.mixin.ObjectPresenter, {
presenter: contact.component.OrganizationPresenter presenter: contact.component.OrganizationPresenter

View File

@ -36,6 +36,7 @@ export default mergeIds(contactId, contact, {
Contacts: '' as AnyComponent, Contacts: '' as AnyComponent,
ContactsTabs: '' as AnyComponent, ContactsTabs: '' as AnyComponent,
EmployeeAccountPresenter: '' as AnyComponent, EmployeeAccountPresenter: '' as AnyComponent,
EmployeeAccountRefPresenter: '' as AnyComponent,
OrganizationEditor: '' as AnyComponent, OrganizationEditor: '' as AnyComponent,
EmployeePresenter: '' as AnyComponent, EmployeePresenter: '' as AnyComponent,
EmployeeRefPresenter: '' as AnyComponent, EmployeeRefPresenter: '' as AnyComponent,

View File

@ -651,7 +651,7 @@ export function createModel (builder: Builder): void {
const applicantViewOptions = (colors: boolean): ViewOptionsModel => { const applicantViewOptions = (colors: boolean): ViewOptionsModel => {
const model: ViewOptionsModel = { const model: ViewOptionsModel = {
groupBy: ['state', 'assignee', 'space'], groupBy: ['state', 'assignee', 'space', 'createdBy', 'modifiedBy'],
orderBy: [ orderBy: [
['state', SortingOrder.Ascending], ['state', SortingOrder.Ascending],
['modifiedOn', SortingOrder.Descending], ['modifiedOn', SortingOrder.Descending],

View File

@ -70,7 +70,7 @@ import {
TimeSpendReport, TimeSpendReport,
trackerId trackerId
} from '@hcengineering/tracker' } from '@hcengineering/tracker'
import { KeyBinding, ViewOptionsModel } from '@hcengineering/view' import { KeyBinding, ViewAction, ViewOptionsModel } from '@hcengineering/view'
import tracker from './plugin' import tracker from './plugin'
import { generateClassNotificationTypes } from '@hcengineering/model-notification' import { generateClassNotificationTypes } from '@hcengineering/model-notification'
@ -385,7 +385,7 @@ export function createModel (builder: Builder): void {
) )
const issuesOptions = (kanban: boolean): ViewOptionsModel => ({ const issuesOptions = (kanban: boolean): ViewOptionsModel => ({
groupBy: ['status', 'assignee', 'priority', 'component', 'milestone'], groupBy: ['status', 'assignee', 'priority', 'component', 'milestone', 'createdBy', 'modifiedBy'],
orderBy: [ orderBy: [
['status', SortingOrder.Ascending], ['status', SortingOrder.Ascending],
['priority', SortingOrder.Descending], ['priority', SortingOrder.Descending],
@ -554,7 +554,7 @@ export function createModel (builder: Builder): void {
) )
const subIssuesOptions: ViewOptionsModel = { const subIssuesOptions: ViewOptionsModel = {
groupBy: ['status', 'assignee', 'priority', 'milestone'], groupBy: ['status', 'assignee', 'priority', 'milestone', 'createdBy', 'modifiedBy'],
orderBy: [ orderBy: [
['rank', SortingOrder.Ascending], ['rank', SortingOrder.Ascending],
['status', SortingOrder.Ascending], ['status', SortingOrder.Ascending],
@ -667,7 +667,7 @@ export function createModel (builder: Builder): void {
attachTo: tracker.class.IssueTemplate, attachTo: tracker.class.IssueTemplate,
descriptor: view.viewlet.List, descriptor: view.viewlet.List,
viewOptions: { viewOptions: {
groupBy: ['assignee', 'priority', 'component', 'milestone'], groupBy: ['assignee', 'priority', 'component', 'milestone', 'createdBy', 'modifiedBy'],
orderBy: [ orderBy: [
['priority', SortingOrder.Ascending], ['priority', SortingOrder.Ascending],
['modifiedOn', SortingOrder.Descending], ['modifiedOn', SortingOrder.Descending],
@ -1060,6 +1060,20 @@ export function createModel (builder: Builder): void {
['backlog', tracker.string.Backlog, {}] ['backlog', tracker.string.Backlog, {}]
] ]
} }
},
{
id: 'all-projects',
component: workbench.component.SpecialView,
icon: view.icon.Archive,
label: tracker.string.AllProjects,
position: 'bottom',
visibleIf: workbench.function.IsOwner,
spaceClass: tracker.class.Project,
componentProps: {
_class: tracker.class.Project,
label: tracker.string.AllIssues,
icon: tracker.icon.Issues
}
} }
], ],
spaces: [ spaces: [
@ -1068,6 +1082,7 @@ export function createModel (builder: Builder): void {
spaceClass: tracker.class.Project, spaceClass: tracker.class.Project,
addSpaceLabel: tracker.string.CreateProject, addSpaceLabel: tracker.string.CreateProject,
createComponent: tracker.component.CreateProject, createComponent: tracker.component.CreateProject,
visibleIf: tracker.function.IsProjectJoined,
icon: tracker.icon.Home, icon: tracker.icon.Home,
specials: [ specials: [
{ {
@ -1162,9 +1177,7 @@ export function createModel (builder: Builder): void {
input: 'focus', input: 'focus',
category: tracker.category.Tracker, category: tracker.category.Tracker,
target: tracker.class.Project, target: tracker.class.Project,
query: { query: {},
archived: false
},
context: { context: {
mode: ['context', 'browser'], mode: ['context', 'browser'],
group: 'edit' group: 'edit'
@ -1182,9 +1195,7 @@ export function createModel (builder: Builder): void {
input: 'focus', input: 'focus',
category: tracker.category.Tracker, category: tracker.category.Tracker,
target: tracker.class.Project, target: tracker.class.Project,
query: { query: {},
archived: false
},
context: { context: {
mode: ['context', 'browser'], mode: ['context', 'browser'],
group: 'edit' group: 'edit'
@ -1213,6 +1224,28 @@ export function createModel (builder: Builder): void {
}, },
tracker.action.DeleteProject tracker.action.DeleteProject
) )
createAction(builder, {
label: tracker.string.Unarchive,
icon: view.icon.Archive,
action: view.actionImpl.UpdateDocument as ViewAction,
actionProps: {
key: 'archived',
ask: true,
value: false,
label: tracker.string.Unarchive,
message: tracker.string.UnarchiveConfirm
},
input: 'any',
category: tracker.category.Tracker,
query: {
archived: true
},
context: {
mode: ['context', 'browser'],
group: 'tools'
},
target: tracker.class.Project
})
createAction( createAction(
builder, builder,
@ -1391,6 +1424,10 @@ export function createModel (builder: Builder): void {
override: [view.action.Open] override: [view.action.Open]
}) })
builder.mixin(tracker.class.Project, core.class.Class, view.mixin.IgnoreActions, {
actions: [view.action.Open]
})
builder.mixin(tracker.class.Issue, core.class.Class, view.mixin.ClassFilters, { builder.mixin(tracker.class.Issue, core.class.Class, view.mixin.ClassFilters, {
filters: [ filters: [
'status', 'status',
@ -1852,7 +1889,7 @@ export function createModel (builder: Builder): void {
) )
const milestoneOptions: ViewOptionsModel = { const milestoneOptions: ViewOptionsModel = {
groupBy: ['status'], groupBy: ['status', 'createdBy', 'modifiedBy'],
orderBy: [ orderBy: [
['modifiedOn', SortingOrder.Descending], ['modifiedOn', SortingOrder.Descending],
['targetDate', SortingOrder.Descending], ['targetDate', SortingOrder.Descending],
@ -1934,7 +1971,7 @@ export function createModel (builder: Builder): void {
) )
const componentListViewOptions: ViewOptionsModel = { const componentListViewOptions: ViewOptionsModel = {
groupBy: ['lead'], groupBy: ['lead', 'createdBy', 'modifiedBy'],
orderBy: [ orderBy: [
['modifiedOn', SortingOrder.Descending], ['modifiedOn', SortingOrder.Descending],
['createdOn', SortingOrder.Descending] ['createdOn', SortingOrder.Descending]
@ -1973,4 +2010,33 @@ export function createModel (builder: Builder): void {
}, },
tracker.viewlet.ComponentList tracker.viewlet.ComponentList
) )
builder.createDoc(
view.class.Viewlet,
core.space.Model,
{
attachTo: tracker.class.Project,
descriptor: view.viewlet.List,
viewOptions: {
groupBy: ['createdBy'],
orderBy: [
['modifiedOn', SortingOrder.Descending],
['createdOn', SortingOrder.Descending]
],
other: [showColorsViewOption]
},
configOptions: {
strict: true,
hiddenKeys: ['label', 'description']
},
config: [
{
key: '',
props: { kind: 'list' }
},
{ key: '', displayProps: { grow: true } }
]
},
tracker.viewlet.ProjectList
)
} }

View File

@ -40,7 +40,10 @@ export default mergeIds(trackerId, tracker, {
CreatedDate: '' as IntlString, CreatedDate: '' as IntlString,
ChangeStatus: '' as IntlString, ChangeStatus: '' as IntlString,
ConfigLabel: '' as IntlString, ConfigLabel: '' as IntlString,
ConfigDescription: '' as IntlString ConfigDescription: '' as IntlString,
Unarchive: '' as IntlString,
UnarchiveConfirm: '' as IntlString,
AllProjects: '' as IntlString
}, },
activity: { activity: {
TxIssueCreated: '' as AnyComponent, TxIssueCreated: '' as AnyComponent,
@ -62,7 +65,8 @@ export default mergeIds(trackerId, tracker, {
IssueTemplateList: '' as Ref<Viewlet>, IssueTemplateList: '' as Ref<Viewlet>,
IssueKanban: '' as Ref<Viewlet>, IssueKanban: '' as Ref<Viewlet>,
MilestoneList: '' as Ref<Viewlet>, MilestoneList: '' as Ref<Viewlet>,
ComponentList: '' as Ref<Viewlet> ComponentList: '' as Ref<Viewlet>,
ProjectList: '' as Ref<Viewlet>
}, },
ids: { ids: {
TxIssueCreated: '' as Ref<TxViewlet>, TxIssueCreated: '' as Ref<TxViewlet>,

View File

@ -13,8 +13,8 @@
// limitations under the License. // limitations under the License.
// //
import { Space } from '@hcengineering/core' import { Doc, Space } from '@hcengineering/core'
import { IntlString, mergeIds, Resource } from '@hcengineering/platform' import { IntlString, Resource, mergeIds } from '@hcengineering/platform'
import { AnyComponent } from '@hcengineering/ui' import { AnyComponent } from '@hcengineering/ui'
import { workbenchId } from '@hcengineering/workbench' import { workbenchId } from '@hcengineering/workbench'
import workbench from '@hcengineering/workbench-resources/src/plugin' import workbench from '@hcengineering/workbench-resources/src/plugin'
@ -32,6 +32,7 @@ export default mergeIds(workbenchId, workbench, {
HiddenApplication: '' as IntlString HiddenApplication: '' as IntlString
}, },
function: { function: {
HasArchiveSpaces: '' as Resource<(spaces: Space[]) => Promise<boolean>> HasArchiveSpaces: '' as Resource<(spaces: Space[]) => Promise<boolean>>,
IsOwner: '' as Resource<(docs: Doc[]) => Promise<boolean>>
} }
}) })

View File

@ -7,6 +7,18 @@
"heftEvent": "clean", "heftEvent": "clean",
"actionId": "defaultClean", "actionId": "defaultClean",
"globsToDelete": ["dist", "lib", "temp"] "globsToDelete": ["dist", "lib", "temp"]
},
{
"actionKind": "copyFiles",
"heftEvent": "pre-compile",
"actionId": "copy-lang",
"copyOperations": [
{
"sourceFolder": "src",
"destinationFolders": ["lib"],
"includeGlobs": ["lang"]
}
]
} }
], ],
"heftPlugins": [ "heftPlugins": [

View File

@ -7,6 +7,18 @@
"heftEvent": "clean", "heftEvent": "clean",
"actionId": "defaultClean", "actionId": "defaultClean",
"globsToDelete": ["dist", "lib", "temp"] "globsToDelete": ["dist", "lib", "temp"]
},
{
"actionKind": "copyFiles",
"heftEvent": "pre-compile",
"actionId": "copy-lang",
"copyOperations": [
{
"sourceFolder": "src",
"destinationFolders": ["lib"],
"includeGlobs": ["lang"]
}
]
} }
], ],
"heftPlugins": [ "heftPlugins": [

View File

@ -14,6 +14,7 @@
"WorkspaceNotFound": "Workspace not found", "WorkspaceNotFound": "Workspace not found",
"InvalidPassword": "Invalid password", "InvalidPassword": "Invalid password",
"AccountAlreadyExists": "Account already exists", "AccountAlreadyExists": "Account already exists",
"WorkspaceRateLimit": "Server is busy, Please wait a bit and try again",
"AccountAlreadyConfirmed": "Account already confirmed", "AccountAlreadyConfirmed": "Account already confirmed",
"AccountWasMerged": "Account was merged", "AccountWasMerged": "Account was merged",
"WorkspaceAlreadyExists": "Workspace already exists", "WorkspaceAlreadyExists": "Workspace already exists",

View File

@ -14,6 +14,7 @@
"WorkspaceNotFound": "Рабочее пространство не найдено", "WorkspaceNotFound": "Рабочее пространство не найдено",
"InvalidPassword": "Неверный пароль", "InvalidPassword": "Неверный пароль",
"AccountAlreadyExists": "Аккаунт уже существует", "AccountAlreadyExists": "Аккаунт уже существует",
"WorkspaceRateLimit": "Сервер перегружен, Пожалуйста подождите",
"AccountAlreadyConfirmed": "Аккаунт уже подтвержден", "AccountAlreadyConfirmed": "Аккаунт уже подтвержден",
"AccountWasMerged": "Аккаунт был объединен", "AccountWasMerged": "Аккаунт был объединен",
"WorkspaceAlreadyExists": "Рабочее пространство уже существует", "WorkspaceAlreadyExists": "Рабочее пространство уже существует",

View File

@ -152,6 +152,7 @@ export default plugin(platformId, {
AccountAlreadyConfirmed: '' as StatusCode<{ account: string }>, AccountAlreadyConfirmed: '' as StatusCode<{ account: string }>,
AccountWasMerged: '' as StatusCode<{ account: string }>, AccountWasMerged: '' as StatusCode<{ account: string }>,
WorkspaceAlreadyExists: '' as StatusCode<{ workspace: string }>, WorkspaceAlreadyExists: '' as StatusCode<{ workspace: string }>,
WorkspaceRateLimit: '' as StatusCode<{ workspace: string }>,
ProductIdMismatch: '' as StatusCode<{ productId: string }> ProductIdMismatch: '' as StatusCode<{ productId: string }>
}, },
metadata: { metadata: {

View File

@ -36,7 +36,7 @@
{#if value} {#if value}
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
{#if employee} {#if employee}
<EmployeePresenter value={employee} {disabled} {inline} {accent} {avatarSize} /> <EmployeePresenter value={employee} {disabled} {inline} {accent} {avatarSize} on:accent-color />
{:else} {:else}
<div class="flex-row-center"> <div class="flex-row-center">
<Avatar size={avatarSize} /> <Avatar size={avatarSize} />

View File

@ -30,5 +30,5 @@
</script> </script>
{#if account} {#if account}
<EmployeeAccountPresenter value={account} {disabled} {inline} {avatarSize} /> <EmployeeAccountPresenter value={account} {disabled} {inline} {avatarSize} on:accent-color />
{/if} {/if}

View File

@ -68,6 +68,7 @@ import EmployeePresenter from './components/EmployeePresenter.svelte'
import EmployeeRefPresenter from './components/EmployeeRefPresenter.svelte' import EmployeeRefPresenter from './components/EmployeeRefPresenter.svelte'
import MemberPresenter from './components/MemberPresenter.svelte' import MemberPresenter from './components/MemberPresenter.svelte'
import Members from './components/Members.svelte' import Members from './components/Members.svelte'
import MembersBox from './components/MembersBox.svelte'
import MembersPresenter from './components/MembersPresenter.svelte' import MembersPresenter from './components/MembersPresenter.svelte'
import MergeEmployee from './components/MergeEmployee.svelte' import MergeEmployee from './components/MergeEmployee.svelte'
import OrganizationEditor from './components/OrganizationEditor.svelte' import OrganizationEditor from './components/OrganizationEditor.svelte'
@ -141,7 +142,8 @@ export {
UserInfo, UserInfo,
IconMembers, IconMembers,
SelectAvatars, SelectAvatars,
UserBoxItems UserBoxItems,
MembersBox
} }
const toObjectSearchResult = (e: WithLookup<Contact>): ObjectSearchResult => ({ const toObjectSearchResult = (e: WithLookup<Contact>): ObjectSearchResult => ({
@ -311,7 +313,8 @@ export default async (): Promise<Resources> => ({
EmployeeFilter, EmployeeFilter,
EmployeeFilterValuePresenter, EmployeeFilterValuePresenter,
EmployeeAccountFilterValuePresenter, EmployeeAccountFilterValuePresenter,
DeleteConfirmationPopup DeleteConfirmationPopup,
EmployeeAccountRefPresenter
}, },
completion: { completion: {
EmployeeQuery: async ( EmployeeQuery: async (

View File

@ -18,7 +18,8 @@
import type { Ref } from '@hcengineering/core' import type { Ref } from '@hcengineering/core'
import core from '@hcengineering/core' import core from '@hcengineering/core'
import { Panel } from '@hcengineering/panel' import { Panel } from '@hcengineering/panel'
import { createQuery, getClient, MembersBox } from '@hcengineering/presentation' import { createQuery, getClient } from '@hcengineering/presentation'
import { MembersBox } from '@hcengineering/contact-resources'
import type { Funnel } from '@hcengineering/lead' import type { Funnel } from '@hcengineering/lead'
import { FullDescriptionBox } from '@hcengineering/text-editor' import { FullDescriptionBox } from '@hcengineering/text-editor'
import { EditBox, Grid } from '@hcengineering/ui' import { EditBox, Grid } from '@hcengineering/ui'

View File

@ -49,7 +49,7 @@
$: queries = { assigned, created, subscribed } $: queries = { assigned, created, subscribed }
$: mode = $resolvedLocationStore.query?.mode ?? undefined $: mode = $resolvedLocationStore.query?.mode ?? undefined
let searchQuery: DocumentQuery<Lead> = { ...baseQuery } let searchQuery: DocumentQuery<Lead> = { ...(baseQuery ?? {}) }
function updateSearchQuery (search: string): void { function updateSearchQuery (search: string): void {
searchQuery = search === '' ? { ...baseQuery } : { ...baseQuery, $search: search } searchQuery = search === '' ? { ...baseQuery } : { ...baseQuery, $search: search }
} }
@ -72,11 +72,11 @@
) )
} }
$: if (mode === 'subscribed') getSubscribed() $: if (mode === 'subscribed') getSubscribed()
$: if (mode === undefined || queries[mode] === undefined) { $: if (mode === undefined || (queries as any)[mode] === undefined) {
;[[mode]] = config ;[[mode]] = config
} }
$: if (mode !== undefined) { $: if (mode !== undefined) {
baseQuery = { ...queries[mode] } baseQuery = { ...((queries as any)[mode] ?? {}) }
modeSelectorProps = { modeSelectorProps = {
config, config,
mode, mode,
@ -101,6 +101,9 @@
.findOne<Viewlet>(view.class.Viewlet, { attachTo: _class, descriptor: task.viewlet.StatusTable }) .findOne<Viewlet>(view.class.Viewlet, { attachTo: _class, descriptor: task.viewlet.StatusTable })
.then((res) => { .then((res) => {
viewlet = res viewlet = res
if (res == null) {
return
}
preferenceQuery.query( preferenceQuery.query(
view.class.ViewletPreference, view.class.ViewletPreference,
{ {

View File

@ -27,7 +27,7 @@
"WantAnotherWorkspace": "Want to create another workspace?", "WantAnotherWorkspace": "Want to create another workspace?",
"ChangeAccount": "Change account", "ChangeAccount": "Change account",
"NotSeeingWorkspace": "Not seeing your workspace?", "NotSeeingWorkspace": "Not seeing your workspace?",
"WorkspaceNameRule": "The workspace name can contain lowercase letters, numbers and symbol -", "WorkspaceNameRule": "The workspace name can contain lowercase letters, numbers and symbol - (inside name)",
"ForgotPassword": "Forgot your password?", "ForgotPassword": "Forgot your password?",
"KnowPassword": "Know your password?", "KnowPassword": "Know your password?",
"Recover": "Recover", "Recover": "Recover",

View File

@ -27,7 +27,7 @@
"WantAnotherWorkspace": "Хотите создать другое рабочее пространство?", "WantAnotherWorkspace": "Хотите создать другое рабочее пространство?",
"ChangeAccount": "Сменить пользователя", "ChangeAccount": "Сменить пользователя",
"NotSeeingWorkspace": "Не видите ваше рабочее пространство?", "NotSeeingWorkspace": "Не видите ваше рабочее пространство?",
"WorkspaceNameRule": "Название рабочего пространства должно состояить из строчных латинских букв, цифр и символа -", "WorkspaceNameRule": "Название рабочего пространства должно состояить из строчных латинских букв, цифр и символа - (внутри имени)",
"ForgotPassword": "Забыли пароль?", "ForgotPassword": "Забыли пароль?",
"KnowPassword": "Знаете пароль?", "KnowPassword": "Знаете пароль?",
"Recover": "Восстановить", "Recover": "Восстановить",

View File

@ -28,7 +28,7 @@
{ {
name: 'workspace', name: 'workspace',
i18n: login.string.Workspace, i18n: login.string.Workspace,
rule: /^[0-9a-z\-)(]{3,63}$/, rule: /^[0-9a-z][0-9a-z-]{2,62}[0-9a-z]$/,
ruleDescr: login.string.WorkspaceNameRule ruleDescr: login.string.WorkspaceNameRule
} }
] ]

View File

@ -15,7 +15,7 @@
<script lang="ts"> <script lang="ts">
import { AttachmentRefInput } from '@hcengineering/attachment-resources' import { AttachmentRefInput } from '@hcengineering/attachment-resources'
import chunter, { Comment } from '@hcengineering/chunter' import chunter, { Comment } from '@hcengineering/chunter'
import { updateBacklinks } from '@hcengineering/chunter-resources/src/backlinks' import { updateBacklinks } from '@hcengineering/chunter-resources'
import { EmployeeAccount } from '@hcengineering/contact' import { EmployeeAccount } from '@hcengineering/contact'
import { AttachedData, getCurrentAccount, Ref } from '@hcengineering/core' import { AttachedData, getCurrentAccount, Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation' import { getClient } from '@hcengineering/presentation'

View File

@ -273,7 +273,10 @@
"NoStatusFound": "No matching status found", "NoStatusFound": "No matching status found",
"CreateMissingStatus": "Create missing status", "CreateMissingStatus": "Create missing status",
"UnsetParent": "Parent issue will be unset" "UnsetParent": "Parent issue will be unset",
"Unarchive": "Unarchive",
"UnarchiveConfirm": "Do you want to unarchive project?",
"AllProjects": "All projects"
}, },
"status": {} "status": {}
} }

View File

@ -272,7 +272,10 @@
"ProjectEmojiiCategory": "Эмодзи", "ProjectEmojiiCategory": "Эмодзи",
"NoStatusFound": "Статус не найдет", "NoStatusFound": "Статус не найдет",
"CreateMissingStatus": "Создать отсутствущий статус", "CreateMissingStatus": "Создать отсутствущий статус",
"UnsetParent": "Родительская задача будет убрана" "UnsetParent": "Родительская задача будет убрана",
"Unarchive": "Разархивировать",
"UnarchiveConfirm": "Вы действительно хотите разархивировать?",
"AllProjects": "All projects"
}, },
"status": {} "status": {}
} }

View File

@ -13,7 +13,17 @@
// limitations under the License. // limitations under the License.
// //
import { Class, Client, Doc, DocumentQuery, Ref, RelatedDocument, toIdMap, TxOperations } from '@hcengineering/core' import {
Class,
Client,
Doc,
DocumentQuery,
getCurrentAccount,
Ref,
RelatedDocument,
toIdMap,
TxOperations
} from '@hcengineering/core'
import { Resources, translate } from '@hcengineering/platform' import { Resources, translate } from '@hcengineering/platform'
import { getClient, MessageBox, ObjectSearchResult } from '@hcengineering/presentation' import { getClient, MessageBox, ObjectSearchResult } from '@hcengineering/presentation'
import { Issue, Milestone, Project } from '@hcengineering/tracker' import { Issue, Milestone, Project } from '@hcengineering/tracker'
@ -419,7 +429,8 @@ export default async (): Promise<Resources> => ({
GetAllPriority: getAllPriority, GetAllPriority: getAllPriority,
GetAllComponents: getAllComponents, GetAllComponents: getAllComponents,
GetAllMilestones: getAllMilestones, GetAllMilestones: getAllMilestones,
GetVisibleFilters: getVisibleFilters GetVisibleFilters: getVisibleFilters,
IsProjectJoined: async (project: Project) => !project.private || project.members.includes(getCurrentAccount()._id)
}, },
actionImpl: { actionImpl: {
Move: move, Move: move,

View File

@ -21,11 +21,11 @@ import {
CreateAggregationManagerFunc, CreateAggregationManagerFunc,
GetAllValuesFunc, GetAllValuesFunc,
GrouppingManagerResource, GrouppingManagerResource,
KeyFilter,
SortFunc, SortFunc,
Viewlet,
ViewletDescriptor,
ViewQueryAction, ViewQueryAction,
KeyFilter Viewlet,
ViewletDescriptor
} from '@hcengineering/view' } from '@hcengineering/view'
import tracker, { trackerId } from '../../tracker/lib' import tracker, { trackerId } from '../../tracker/lib'
@ -382,7 +382,8 @@ export default mergeIds(trackerId, tracker, {
GetAllPriority: '' as GetAllValuesFunc, GetAllPriority: '' as GetAllValuesFunc,
GetAllComponents: '' as GetAllValuesFunc, GetAllComponents: '' as GetAllValuesFunc,
GetAllMilestones: '' as GetAllValuesFunc, GetAllMilestones: '' as GetAllValuesFunc,
GetVisibleFilters: '' as Resource<(filters: KeyFilter[], space?: Ref<Space>) => Promise<KeyFilter[]>> GetVisibleFilters: '' as Resource<(filters: KeyFilter[], space?: Ref<Space>) => Promise<KeyFilter[]>>,
IsProjectJoined: '' as Resource<(space: Space) => Promise<boolean>>
}, },
aggregation: { aggregation: {
CreateComponentAggregationManager: '' as CreateAggregationManagerFunc, CreateComponentAggregationManager: '' as CreateAggregationManagerFunc,

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { Class, Doc, Ref, Space, WithLookup } from '@hcengineering/core' import { Class, Doc, DocumentQuery, Ref, Space, WithLookup } from '@hcengineering/core'
import { Asset, IntlString } from '@hcengineering/platform' import { Asset, IntlString } from '@hcengineering/platform'
import { createQuery } from '@hcengineering/presentation' import { createQuery } from '@hcengineering/presentation'
import { import {
@ -50,6 +50,7 @@
export let createComponentProps: Record<string, any> = {} export let createComponentProps: Record<string, any> = {}
export let isCreationDisabled = false export let isCreationDisabled = false
export let descriptors: Ref<ViewletDescriptor>[] | undefined = undefined export let descriptors: Ref<ViewletDescriptor>[] | undefined = undefined
export let baseQuery: DocumentQuery<Doc> | undefined = undefined
let search = '' let search = ''
let viewlet: WithLookup<Viewlet> | undefined let viewlet: WithLookup<Viewlet> | undefined
@ -116,7 +117,7 @@
preferenceQuery.unsubscribe() preferenceQuery.unsubscribe()
} }
$: query = viewlet?.baseQuery ?? {} $: query = { ...(baseQuery ?? {}), ...(viewlet?.baseQuery ?? {}) }
$: searchQuery = search === '' ? query : { ...query, $search: search } $: searchQuery = search === '' ? query : { ...query, $search: search }
$: resultQuery = searchQuery $: resultQuery = searchQuery
@ -186,7 +187,7 @@
</div> </div>
</div> </div>
{#if !viewlet?.$lookup?.descriptor?.component || viewlet?.attachTo !== _class || (preference !== undefined && viewlet?._id !== preference.attachedTo)}} {#if !viewlet?.$lookup?.descriptor?.component || viewlet?.attachTo !== _class || (preference !== undefined && viewlet?._id !== preference.attachedTo)}
<Loading /> <Loading />
{:else} {:else}
<FilterBar <FilterBar

View File

@ -117,6 +117,32 @@
return await getResource(value.presenter) return await getResource(value.presenter)
} }
} }
let visibleIf: ((space: Space) => Promise<boolean>) | undefined
$: if (model.visibleIf) {
getResource(model.visibleIf).then((r) => {
visibleIf = r
})
}
let filteredSpaces: Space[] = []
async function updateSpaces (spaces: Space[], visibleIf: (space: Space) => Promise<boolean>): Promise<void> {
const result: Space[] = []
for (const s of spaces) {
if (await visibleIf(s)) {
result.push(s)
}
}
filteredSpaces = result
}
$: if (visibleIf) {
updateSpaces(spaces, visibleIf)
} else {
filteredSpaces = spaces
}
</script> </script>
<TreeNode <TreeNode
@ -125,7 +151,7 @@
actions={async () => getParentActions()} actions={async () => getParentActions()}
shortDropbox={model.specials !== undefined} shortDropbox={model.specials !== undefined}
> >
{#each spaces as space, i (space._id)} {#each filteredSpaces as space, i (space._id)}
{#await getPresenter(space._class) then presenter} {#await getPresenter(space._class) then presenter}
{#if separate && model.specials && i !== 0}<TreeSeparator line />{/if} {#if separate && model.specials && i !== 0}<TreeSeparator line />{/if}
{#if model.specials && presenter} {#if model.specials && presenter}

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
// //
import { Space } from '@hcengineering/core' import { AccountRole, Space, getCurrentAccount } from '@hcengineering/core'
import { Resources } from '@hcengineering/platform' import { Resources } from '@hcengineering/platform'
import ApplicationPresenter from './components/ApplicationPresenter.svelte' import ApplicationPresenter from './components/ApplicationPresenter.svelte'
import Archive from './components/Archive.svelte' import Archive from './components/Archive.svelte'
@ -42,7 +42,8 @@ export default async (): Promise<Resources> => ({
Workbench Workbench
}, },
function: { function: {
HasArchiveSpaces: hasArchiveSpaces HasArchiveSpaces: hasArchiveSpaces,
IsOwner: async (docs: Space[]) => getCurrentAccount().role === AccountRole.Owner
}, },
actionImpl: { actionImpl: {
Navigate: doNavigate Navigate: doNavigate

View File

@ -59,6 +59,8 @@ export interface SpacesNavModel {
// Child special items. // Child special items.
specials?: SpecialNavModel[] specials?: SpecialNavModel[]
visibleIf?: Resource<(space: Space) => Promise<boolean>>
} }
/** /**

View File

@ -97,7 +97,6 @@ export function serveAccount (methods: Record<string, AccountMethod>, productId
} }
const db = client.db(ACCOUNT_DB) const db = client.db(ACCOUNT_DB)
const result = await method(db, productId, request, token) const result = await method(db, productId, request, token)
console.log(result)
ctx.body = result ctx.body = result
}) })

View File

@ -104,6 +104,7 @@ export interface Account {
// Defined for server admins only // Defined for server admins only
admin?: boolean admin?: boolean
confirmed?: boolean confirmed?: boolean
lastWorkspace?: number
} }
/** /**
@ -115,6 +116,7 @@ export interface Workspace {
organisation: string organisation: string
accounts: ObjectId[] accounts: ObjectId[]
productId: string productId: string
disabled?: boolean
} }
/** /**
@ -285,6 +287,9 @@ export async function selectWorkspace (
const workspaceInfo = await getWorkspace(db, productId, workspace) const workspaceInfo = await getWorkspace(db, productId, workspace)
if (workspaceInfo !== null) { if (workspaceInfo !== null) {
if (workspaceInfo.disabled === true) {
throw new PlatformError(new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspace }))
}
const workspaces = accountInfo.workspaces const workspaces = accountInfo.workspaces
for (const w of workspaces) { for (const w of workspaces) {
@ -347,6 +352,7 @@ export async function join (
): Promise<WorkspaceLoginInfo> { ): Promise<WorkspaceLoginInfo> {
const invite = await getInvite(db, inviteId) const invite = await getInvite(db, inviteId)
const workspace = await checkInvite(invite, email) const workspace = await checkInvite(invite, email)
console.log(`join attempt:${email}, ${workspace.name}`)
await assignWorkspace(db, productId, email, workspace.name) await assignWorkspace(db, productId, email, workspace.name)
const token = (await login(db, productId, email, password)).token const token = (await login(db, productId, email, password)).token
@ -360,6 +366,7 @@ export async function join (
*/ */
export async function confirmEmail (db: Db, email: string): Promise<Account> { export async function confirmEmail (db: Db, email: string): Promise<Account> {
const account = await getAccount(db, email) const account = await getAccount(db, email)
console.log(`confirm email:${email}`)
if (account === null) { if (account === null) {
throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, { account: accountId })) throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, { account: accountId }))
@ -446,6 +453,7 @@ export async function signUpJoin (
last: string, last: string,
inviteId: ObjectId inviteId: ObjectId
): Promise<WorkspaceLoginInfo> { ): Promise<WorkspaceLoginInfo> {
console.log(`signup join:${email} ${first} ${last}`)
const invite = await getInvite(db, inviteId) const invite = await getInvite(db, inviteId)
const workspace = await checkInvite(invite, email) const workspace = await checkInvite(invite, email)
await createAcc(db, productId, email, password, first, last, invite?.emailMask === email) await createAcc(db, productId, email, password, first, last, invite?.emailMask === email)
@ -529,6 +537,7 @@ export async function createAccount (
export async function listWorkspaces (db: Db, productId: string): Promise<WorkspaceInfoOnly[]> { export async function listWorkspaces (db: Db, productId: string): Promise<WorkspaceInfoOnly[]> {
return (await db.collection<Workspace>(WORKSPACE_COLLECTION).find(withProductId(productId, {})).toArray()) return (await db.collection<Workspace>(WORKSPACE_COLLECTION).find(withProductId(productId, {})).toArray())
.map((it) => ({ ...it, productId })) .map((it) => ({ ...it, productId }))
.filter((it) => it.disabled !== true)
.map(trimWorkspace) .map(trimWorkspace)
} }
@ -612,20 +621,59 @@ export async function upgradeWorkspace (
export const createUserWorkspace = export const createUserWorkspace =
(version: Data<Version>, txes: Tx[], migrationOperation: [string, MigrateOperation][]) => (version: Data<Version>, txes: Tx[], migrationOperation: [string, MigrateOperation][]) =>
async (db: Db, productId: string, token: string, workspace: string): Promise<LoginInfo> => { async (db: Db, productId: string, token: string, workspace: string): Promise<LoginInfo> => {
if (!/^[0-9a-z][0-9a-z-]{2,62}[0-9a-z]$/.test(workspace)) {
throw new PlatformError(new Status(Severity.ERROR, platform.status.InvalidId, { id: workspace }))
}
const { email, extra } = decodeToken(token) const { email, extra } = decodeToken(token)
if (extra?.confirmed === false) { const nonConfirmed = extra?.confirmed === false
console.log(`Creating workspace ${workspace} for ${email} ${nonConfirmed ? 'non confirmed' : 'confirmed'}`)
if (nonConfirmed) {
throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, { account: email })) throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, { account: email }))
} }
await createWorkspace(version, txes, migrationOperation, db, productId, workspace, '') const info = await getAccount(db, email)
if (info === null) {
throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, { account: email }))
}
if (info.lastWorkspace !== undefined) {
if (Date.now() - info.lastWorkspace < 60 * 1000) {
throw new PlatformError(new Status(Severity.ERROR, platform.status.WorkspaceRateLimit, { workspace }))
}
}
try {
await createWorkspace(version, txes, migrationOperation, db, productId, workspace, '')
} catch (err: any) {
console.error(err)
// We need to drop workspace, to prevent wrong data usage.
const ws = await getWorkspace(db, productId, workspace)
if (ws === null) {
throw new PlatformError(new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspace }))
}
await db.collection(WORKSPACE_COLLECTION).updateOne(
{
_id: ws._id
},
{ $set: { disabled: true } }
)
throw err
}
info.lastWorkspace = Date.now()
// Update last workspace time.
await db.collection(ACCOUNT_COLLECTION).updateOne({ _id: info._id }, { $set: { lastWorkspace: Date.now() } })
await assignWorkspace(db, productId, email, workspace) await assignWorkspace(db, productId, email, workspace)
await setRole(email, workspace, productId, AccountRole.Owner) await setRole(email, workspace, productId, AccountRole.Owner)
const info = await getAccount(db, email)
const result = { const result = {
endpoint: getEndpoint(), endpoint: getEndpoint(),
email, email,
token: generateToken(email, getWorkspaceId(workspace, productId), getExtra(info)), token: generateToken(email, getWorkspaceId(workspace, productId), getExtra(info)),
productId productId
} }
console.log(`Creating workspace ${workspace} Done`)
return result return result
} }
@ -678,7 +726,9 @@ export async function getUserWorkspaces (db: Db, productId: string, token: strin
.collection<Workspace>(WORKSPACE_COLLECTION) .collection<Workspace>(WORKSPACE_COLLECTION)
.find(withProductId(productId, account.admin === true ? {} : { _id: { $in: account.workspaces } })) .find(withProductId(productId, account.admin === true ? {} : { _id: { $in: account.workspaces } }))
.toArray() .toArray()
).map(trimWorkspace) )
.filter((it) => it.disabled !== true)
.map(trimWorkspace)
} }
async function getWorkspaceAndAccount ( async function getWorkspaceAndAccount (

View File

@ -57,17 +57,24 @@ export class MinioService {
} }
async list (workspaceId: WorkspaceId, prefix?: string): Promise<MinioWorkspaceItem[]> { async list (workspaceId: WorkspaceId, prefix?: string): Promise<MinioWorkspaceItem[]> {
const items = new Map<string, BucketItem & { metaData: ItemBucketMetadata }>() try {
const list = await this.client.listObjects(getBucketId(workspaceId), prefix, true) const items = new Map<string, BucketItem & { metaData: ItemBucketMetadata }>()
await new Promise((resolve) => { const list = await this.client.listObjects(getBucketId(workspaceId), prefix, true)
list.on('data', (data) => { await new Promise((resolve) => {
items.set(data.name, { metaData: {}, ...data }) list.on('data', (data) => {
items.set(data.name, { metaData: {}, ...data })
})
list.on('end', () => {
resolve(null)
})
}) })
list.on('end', () => { return Array.from(items.values())
resolve(null) } catch (err: any) {
}) if (((err?.message as string) ?? '').includes('Invalid bucket name')) {
}) return []
return Array.from(items.values()) }
throw err
}
} }
async stat (workspaceId: WorkspaceId, objectName: string): Promise<BucketItemStat> { async stat (workspaceId: WorkspaceId, objectName: string): Promise<BucketItemStat> {