Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/shaggy-foxes-remain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@xpert-ai/plugin-sdk': patch
'@xpert-ai/contracts': patch
'@xpert-ai/xpert-ui': patch
---

collaboration & artifacts
2 changes: 1 addition & 1 deletion .deploy/webapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
"@sentry/tracing": "^7.38.0",
"@swc/helpers": "0.5.3",
"@tinymce/tinymce-angular": "^6.0.1",
"@xpert-ai/chatkit-ui": "~0.4.5",
"@xpert-ai/chatkit-ui": "~0.4.7",
"angular-gridster2": "^14.0.1",
"apache-arrow": "^9.0.0",
"axios": "1.6.8",
Expand Down
2 changes: 1 addition & 1 deletion apps/cloud/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0",
"@xpert-ai/chatkit-angular": "~0.4.0",
"@xpert-ai/chatkit-ui": "~0.4.6",
"@xpert-ai/chatkit-ui": "~0.4.7",
"@xpert-ai/chatkit-types": "~0.4.5",
"@xpert-ai/chatkit-web-component": "~0.4.0",
"@xpert-ai/chatkit-web-shared": "~0.4.0",
Expand Down
10 changes: 8 additions & 2 deletions apps/cloud/proxy.conf.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
{
"/api": {
"target": "http://localhost:3000",
"secure": false
"target": "http://127.0.0.1:3000",
"secure": false,
"changeOrigin": true
},
"/artifacts/share": {
"target": "http://127.0.0.1:3000",
"secure": false,
"changeOrigin": true
}
}
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
<section class="space-y-8">
<div
class="flex flex-col gap-3 rounded-lg border border-border bg-card p-4 sm:flex-row sm:items-center sm:justify-between"
>
<div>
<div class="text-sm font-semibold text-foreground">
{{ 'PAC.Explore.AgentSquare.ReviewTitle' | translate: { Default: 'Requests to review' } }}
</div>
<div class="mt-1 text-sm text-muted-foreground">
{{
'PAC.Explore.AgentSquare.ReviewSubtitle'
| translate: { Default: 'Approve or reject pending agent access requests.' }
}}
@if (reviewableCount() > 0) {
<div
class="flex flex-col gap-3 rounded-lg border border-border bg-card p-4 sm:flex-row sm:items-center sm:justify-between"
>
<div>
<div class="text-sm font-semibold text-foreground">
{{ 'PAC.Explore.AgentSquare.ReviewTitle' | translate: { Default: 'Requests to review' } }}
</div>
<div class="mt-1 text-sm text-muted-foreground">
{{
'PAC.Explore.AgentSquare.ReviewSubtitle'
| translate: { Default: 'Approve or reject pending agent access requests.' }
}}
</div>
</div>
<button z-button zSize="sm" zType="outline" type="button" class="w-fit" (click)="openReviewDialog()">
<z-icon zType="inbox" zSize="sm" />
{{ 'PAC.Explore.AgentSquare.ReviewEntry' | translate: { Default: 'Review' } }}
<span class="ml-1 rounded-md bg-primary px-1.5 py-0.5 text-xs text-primary-foreground">{{
reviewableCount()
}}</span>
</button>
</div>
<button z-button zSize="sm" zType="outline" type="button" class="w-fit" (click)="openReviewDialog()">
<z-icon zType="inbox" zSize="sm" />
{{ 'PAC.Explore.AgentSquare.ReviewEntry' | translate: { Default: 'Review' } }}
<span class="ml-1 rounded-md bg-primary px-1.5 py-0.5 text-xs text-primary-foreground">{{
reviewableCount()
}}</span>
</button>
</div>
}

<div class="grid gap-5 xl:grid-cols-[minmax(0,1fr)_320px]">
<section
Expand Down
25 changes: 24 additions & 1 deletion apps/cloud/src/app/features/features-routing.module.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
jest.mock('../@core', () => ({
AiFeatureEnum: {
FEATURE_XPERT: 'FEATURE_XPERT',
FEATURE_XPERT_MARKETPLACE: 'FEATURE_XPERT_MARKETPLACE'
},
AnalyticsPermissionsEnum: {
STORIES_VIEW: 'STORIES_VIEW',
STORIES_EDIT: 'STORIES_EDIT',
Expand All @@ -9,11 +13,20 @@ jest.mock('../@core', () => ({
XPERT_EDIT: 'XPERT_EDIT'
},
RolesEnum: {
ADMIN: 'ADMIN',
AI_BUILDER: 'AI_BUILDER',
SUPER_ADMIN: 'SUPER_ADMIN'
},
authGuard: jest.fn()
}))

jest.mock('./feature-gate', () => ({
featureGate: jest.fn((featureKeys: string[], redirectCommands: string[]) => ({
featureKeys,
redirectCommands
}))
}))

jest.mock('./features.component', () => ({
FeaturesComponent: class FeaturesComponent {}
}))
Expand All @@ -31,7 +44,7 @@ jest.mock('../app.service', () => ({
}))

import { NgxPermissionsGuard } from 'ngx-permissions'
import { routes } from './features-routing.module'
import { routes, xpertMarketplaceRouteGate } from './features-routing.module'

describe('features routing', () => {
const children = routes[0].children ?? []
Expand Down Expand Up @@ -73,6 +86,16 @@ describe('features routing', () => {
expect(route?.data?.permissions?.only).toEqual(['SUPER_ADMIN'])
})

it('mounts xpert access requests at the top level behind marketplace and reviewer role gates', () => {
const route = children.find((item) => item.path === 'xpert-access-requests')

expect(route?.loadComponent).toEqual(expect.any(Function))
expect(route?.canActivate).toContain(NgxPermissionsGuard)
expect(route?.canActivate).toContain(xpertMarketplaceRouteGate)
expect(route?.data?.scopeContext).toBe('organization-only')
expect(route?.data?.permissions?.only).toEqual(['AI_BUILDER', 'ADMIN', 'SUPER_ADMIN'])
})

it('mounts model providers at the top-level /copilot route', () => {
const route = children.find((item) => item.path === 'copilot')

Expand Down
22 changes: 21 additions & 1 deletion apps/cloud/src/app/features/features-routing.module.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import { inject, NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router'
import { NgxPermissionsGuard } from 'ngx-permissions'
import { AIPermissionsEnum, AnalyticsPermissionsEnum, RolesEnum, authGuard } from '../@core'
import { AiFeatureEnum, AIPermissionsEnum, AnalyticsPermissionsEnum, RolesEnum, authGuard } from '../@core'
import { FeaturesComponent } from './features.component'
import { NotFoundComponent } from '../@shared/not-found'
import { AppService } from '../app.service'
import { featureGate } from './feature-gate'

export function redirectTo() {
return '/chat'
}

export const xpertMarketplaceRouteGate = featureGate(
[AiFeatureEnum.FEATURE_XPERT, AiFeatureEnum.FEATURE_XPERT_MARKETPLACE],
['/explore']
)

export const routes: Routes = [
{
path: '',
Expand Down Expand Up @@ -136,6 +142,20 @@ export const routes: Routes = [
}
}
},
{
path: 'xpert-access-requests',
loadComponent: () =>
import('./xpert-access-requests/xpert-access-requests.component').then((m) => m.XpertAccessRequestsComponent),
canActivate: [authGuard, NgxPermissionsGuard, xpertMarketplaceRouteGate],
data: {
title: 'Xpert Access Requests',
scopeContext: 'organization-only',
permissions: {
only: [RolesEnum.AI_BUILDER, RolesEnum.ADMIN, RolesEnum.SUPER_ADMIN],
redirectTo
}
}
},
{
path: 'copilot',
loadChildren: () => import('./setting/copilot/routing').then((m) => m.default),
Expand Down
6 changes: 2 additions & 4 deletions apps/cloud/src/app/features/menus.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,10 +137,8 @@ describe('getFeatureMenus', () => {

it('promotes xpert access requests to the management menu with approval gates', () => {
const menus = getFeatureMenus(RequestScopeLevel.ORGANIZATION, null)
const settings = menus.find((item) => item.link === '/settings')
const requests = menus.find((item) => item.link === '/settings/xpert-access-requests')
const requests = menus.find((item) => item.link === '/xpert-access-requests')

expect(settings?.data?.inactivePathPrefixes).toEqual(['/settings/xpert-access-requests'])
expect(requests).toMatchObject({
title: 'Xpert Access Requests',
icon: 'approval',
Expand All @@ -153,7 +151,7 @@ describe('getFeatureMenus', () => {
AiFeatureEnum.FEATURE_XPERT_MARKETPLACE,
FeatureEnum.FEATURE_USER_GROUPS
])
expect(requests?.data?.permissionKeys).toEqual([PermissionsEnum.ORG_USERS_VIEW, PermissionsEnum.ORG_USERS_EDIT])
expect(requests?.data?.permissionKeys).toEqual([RolesEnum.AI_BUILDER, RolesEnum.ADMIN, RolesEnum.SUPER_ADMIN])
})

it('marks the workspace menu as an onboarding target', () => {
Expand Down
7 changes: 3 additions & 4 deletions apps/cloud/src/app/features/menus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,14 +379,13 @@ export function getFeatureMenus(scopeLevel: RequestScopeLevel, _org: IOrganizati
admin: true,
scopeContext: 'dual-scope',
data: {
translationKey: 'Settings',
inactivePathPrefixes: ['/settings/xpert-access-requests']
translationKey: 'Settings'
}
},
{
title: 'Xpert Access Requests',
icon: 'approval',
link: '/settings/xpert-access-requests',
link: '/xpert-access-requests',
admin: true,
scopeContext: 'organization-only',
data: {
Expand All @@ -396,7 +395,7 @@ export function getFeatureMenus(scopeLevel: RequestScopeLevel, _org: IOrganizati
AiFeatureEnum.FEATURE_XPERT_MARKETPLACE,
FeatureEnum.FEATURE_USER_GROUPS
],
permissionKeys: [PermissionsEnum.ORG_USERS_VIEW, PermissionsEnum.ORG_USERS_EDIT]
permissionKeys: [RolesEnum.AI_BUILDER, RolesEnum.ADMIN, RolesEnum.SUPER_ADMIN]
}
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,7 @@ jest.mock('./settings.component', () => ({
}))

import { NgxPermissionsGuard } from 'ngx-permissions'
import {
membershipPlanAccountGate,
membershipPlanSettingsGate,
routes,
xpertMarketplaceSettingsGate
} from './setting-routing.module'
import { membershipPlanAccountGate, membershipPlanSettingsGate, routes } from './setting-routing.module'

describe('setting routes', () => {
const settingChildren = routes[0].children ?? []
Expand All @@ -72,10 +67,4 @@ describe('setting routes', () => {
expect(usageRoute?.canActivate).toEqual([membershipPlanAccountGate])
expect(billingRoute?.canActivate).toEqual([membershipPlanAccountGate])
})

it('guards xpert access request approvals with the marketplace feature gate', () => {
const accessRequestsRoute = settingChildren.find((route) => route.path === 'xpert-access-requests')

expect(accessRequestsRoute?.canActivate).toEqual([NgxPermissionsGuard, xpertMarketplaceSettingsGate])
})
})
21 changes: 0 additions & 21 deletions apps/cloud/src/app/features/setting/setting-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,6 @@ export const membershipPlanAccountGate = featureGate(
[AiFeatureEnum.FEATURE_MEMBERSHIP_PLAN],
['/settings/account/profile']
)
export const xpertMarketplaceSettingsGate = featureGate(
[AiFeatureEnum.FEATURE_XPERT, AiFeatureEnum.FEATURE_XPERT_MARKETPLACE],
['/settings']
)

export const routes: Routes = [
{
path: '',
Expand Down Expand Up @@ -141,22 +136,6 @@ export const routes: Routes = [
}
}
},
{
path: 'xpert-access-requests',
loadComponent: () =>
import('./xpert-access-requests/xpert-access-requests.component').then(
(m) => m.XpertAccessRequestsSettingsComponent
),
canActivate: [NgxPermissionsGuard, xpertMarketplaceSettingsGate],
data: {
title: 'settings/xpert-access-requests',
scopeContext: 'organization-only',
permissions: {
only: [PermissionsEnum.ORG_USERS_VIEW],
redirectTo
}
}
},
{
path: 'business-area',
loadChildren: () => import('./business-area/').then((m) => m.routes),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,15 +155,15 @@
[cdkDragDisabled]="collapsed()"
>
<div
class="cloud-sidebar-assistants__item relative"
class="cloud-sidebar-assistants__item group relative"
[class.is-active]="isActive(xpert)"
[zTooltip]="assistantLabel(xpert)"
zPosition="right"
[zDisabled]="!collapsed()"
>
@if (!collapsed()) {
<i
class="ri-draggable w-3 shrink-0 cursor-grab touch-none text-text-tertiary opacity-50 transition-opacity hover:opacity-100 active:cursor-grabbing"
class="ri-draggable pointer-events-none w-3 -translate-x-1 shrink-0 cursor-grab touch-none text-text-tertiary opacity-0 transition-opacity duration-200 ease-out group-hover:pointer-events-auto group-hover:opacity-100 group-focus-within:pointer-events-auto group-focus-within:opacity-100 motion-reduce:transition-none active:cursor-grabbing"
cdkDragHandle
aria-hidden="true"
></i>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import {
getAssistantLabel,
getAssistantRouteId,
isAssistantRouteActive,
normalizeAssistantXperts
normalizeAssistantXperts,
orderAssistantXperts
} from './cloud-sidebar-assistants.utils'

jest.mock('@xpert-ai/headless-ui', () => {
Expand Down Expand Up @@ -139,6 +140,32 @@ describe('cloud sidebar assistants helpers', () => {
expect(filterAssistantXperts(items, 'tool').map((item) => item.id)).toEqual(['tools'])
})

it('places assistants missing from the saved order first by newest creation time', () => {
const items = [
xpert({ id: 'ordered-first', createdAt: new Date('2026-01-03T00:00:00Z') }),
xpert({ id: 'newer', createdAt: new Date('2026-01-05T00:00:00Z') }),
xpert({ id: 'ordered-second', createdAt: new Date('2026-01-04T00:00:00Z') }),
xpert({ id: 'newest', createdAt: new Date('2026-01-06T00:00:00Z') })
]

expect(orderAssistantXperts(items, ['ordered-first', 'ordered-second']).map((item) => item.id)).toEqual([
'newest',
'newer',
'ordered-first',
'ordered-second'
])
})

it('orders all assistants by newest creation time when no saved order exists', () => {
const items = [
xpert({ id: 'oldest', createdAt: new Date('2026-01-01T00:00:00Z') }),
xpert({ id: 'newest', createdAt: new Date('2026-01-03T00:00:00Z') }),
xpert({ id: 'middle', createdAt: new Date('2026-01-02T00:00:00Z') })
]

expect(orderAssistantXperts(items, []).map((item) => item.id)).toEqual(['newest', 'middle', 'oldest'])
})

it('matches assistant categories from tag names instead of label or description keywords', () => {
const items = [
xpert({ id: 'finance', title: 'General Assistant', tags: [{ name: 'Finance' }] }),
Expand Down
Loading
Loading