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
2 changes: 1 addition & 1 deletion application/single_app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@
EXECUTOR_TYPE = 'thread'
EXECUTOR_MAX_WORKERS = 30
SESSION_TYPE = 'filesystem'
VERSION = "0.261.295"
VERSION = "0.261.296"
IS_DEVELOPMENT = is_development_env_enabled()

# Opt-out for deployments where App Service Easy Auth is active but the platform
Expand Down
68 changes: 49 additions & 19 deletions application/v2_ui/src/components/settings/WorkspaceListTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
// been fetched would quietly hide matches.

import { useCallback, useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { clsx } from 'clsx';
import { Check, Globe2, Loader2, Search, Users } from 'lucide-react';
import { ArrowUpRight, Check, Globe2, Loader2, Search, Users } from 'lucide-react';

Check warning on line 11 in application/v2_ui/src/components/settings/WorkspaceListTab.tsx

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
import { ApiError } from '../../lib/apiClient';
import type { WorkspaceKind, WorkspaceSummary } from '../../lib/workspaces';
import { GlassPanel, Skeleton } from '../ui/primitives';
Expand All @@ -23,13 +24,14 @@
return null;
}
return (
<span className="rounded-full border border-edge px-2 py-0.5 text-[11px] text-text-3">
<span className="shrink-0 rounded-full border border-edge px-2 py-0.5 text-[11px] text-text-3">
{role}
</span>
);
}

export function WorkspaceListTab({ kind }: { kind: WorkspaceKind }) {
const navigate = useNavigate();
const needsReconciliation = useGroupWorkspaceStore((state) => kind.scope === 'group' && state.needsReconciliation);
const [items, setItems] = useState<WorkspaceSummary[]>([]);
const [page, setPage] = useState(1);
Expand Down Expand Up @@ -128,6 +130,18 @@
}
};

// Open never activates from here; the page it lands on decides, as with the directories.
const openWorkspace = (workspace: WorkspaceSummary) => {
let path: string;
try {
path = kind.openPath(workspace.id);
} catch {
setActionError(`Could not open that ${kind.noun}.`);
return;
}
navigate(path);
};

const lastPage = Math.max(1, Math.ceil(totalCount / PAGE_SIZE));
const title = `Your ${kind.pluralNoun}`;

Expand Down Expand Up @@ -186,15 +200,17 @@
<ul className="space-y-2">
{items.map((workspace) => (
<li key={workspace.id}>
<GlassPanel className="flex items-center gap-3 p-3">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
{/* Wraps like the directory rows: on a narrow screen the actions drop
below the name instead of squeezing it to a few letters. */}
<GlassPanel className="flex flex-wrap items-center gap-3 p-3">
<div className="min-w-0 flex-1 basis-48">
<div className="flex min-w-0 items-center gap-2">
<span className="truncate text-sm font-medium text-text-1">
{workspace.name || 'Untitled'}
</span>
<RoleBadge role={workspace.userRole} />
{workspace.status && workspace.status !== 'active' && (
<span className="rounded-full bg-warn-soft px-2 py-0.5 text-[11px] text-warn">
<span className="shrink-0 rounded-full bg-warn-soft px-2 py-0.5 text-[11px] text-warn">
{workspace.status}
</span>
)}
Expand All @@ -206,27 +222,41 @@
)}
</div>

{workspace.isActive ? (
<span className="flex shrink-0 items-center gap-1.5 rounded-lg bg-ok-soft px-2.5 py-1.5 text-xs font-medium text-ok">
<Check size={13} /> Active
</span>
) : (
<div className="ml-auto flex shrink-0 items-center gap-2">
{workspace.isActive ? (
<span className="flex shrink-0 items-center gap-1.5 rounded-lg bg-ok-soft px-2.5 py-1.5 text-xs font-medium text-ok">
<Check size={13} /> Active
</span>
) : (
<button
type="button"
onClick={() => void activate(workspace)}
disabled={activating !== null || needsReconciliation}

Check warning on line 234 in application/v2_ui/src/components/settings/WorkspaceListTab.tsx

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
className={clsx(
'shrink-0 rounded-lg border border-edge px-2.5 py-1.5 text-xs font-medium text-text-1',
'hover:bg-surface-2 disabled:opacity-60',

Check warning on line 237 in application/v2_ui/src/components/settings/WorkspaceListTab.tsx

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
)}
>
{activating === workspace.id ? (
<Loader2 size={13} className="animate-spin" />
) : (
'Set active'
)}
</button>
)}
<button
type="button"
onClick={() => void activate(workspace)}
onClick={() => openWorkspace(workspace)}
disabled={activating !== null || needsReconciliation}
aria-label={`Open ${workspace.name || 'Untitled'}`}
className={clsx(
'shrink-0 rounded-lg border border-edge px-2.5 py-1.5 text-xs font-medium text-text-1',
'flex shrink-0 items-center gap-1 rounded-lg border border-edge px-2.5 py-1.5 text-xs font-medium text-text-1',
'hover:bg-surface-2 disabled:opacity-60',
)}
>
{activating === workspace.id ? (
<Loader2 size={13} className="animate-spin" />
) : (
'Set active'
)}
Open<ArrowUpRight size={13} />
</button>
)}
</div>
</GlassPanel>
</li>
))}
Expand Down
10 changes: 10 additions & 0 deletions application/v2_ui/src/lib/workspaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
// GET; the dedicated setActive routes say plainly what they do and report why they refused.

import { api } from './apiClient';
import { groupWorkspacePath } from './groupWorkspaceNavigation';
import { publicWorkspacePath } from './publicWorkspaceNavigation';
import { isRecord } from './workspaceAuthoring';
import { useBootstrapStore } from '../stores/bootstrapStore';

Expand Down Expand Up @@ -54,6 +56,12 @@ export interface WorkspaceKind {
list: (page: number, pageSize: number, search: string, signal?: AbortSignal) => Promise<WorkspacePage>;
/** Make one active. Rejects with a message the server supplied. */
setActive: (id: string) => Promise<void>;
/**
* Where Open goes for a row: the V2 workspace page, addressed by its immutable id. Opening
* never activates anything here; as with the directories' Open, the page decides. A group
* page activates the group it shows, and a public page leaves the active one alone.
*/
openPath: (id: string) => string;
/** What one of these is called, for empty states and labels. */
noun: string;
pluralNoun: string;
Expand Down Expand Up @@ -107,6 +115,7 @@ export const GROUP_WORKSPACES: WorkspaceKind = {
// 400 missing id, 404 unknown group, 403 not a member — all surfaced as written.
await api.patch('/api/groups/setActive', { groupId: id });
},
openPath: (id) => groupWorkspacePath(id),
noun: 'group',
pluralNoun: 'groups',
classicHref: '/profile?tab=groups',
Expand Down Expand Up @@ -137,6 +146,7 @@ export const PUBLIC_WORKSPACES: WorkspaceKind = {
setActive: async (id) => {
await api.patch('/api/public_workspaces/setActive', { workspaceId: id });
},
openPath: (id) => publicWorkspacePath(id),
noun: 'public workspace',
pluralNoun: 'public workspaces',
classicHref: '/profile?tab=public-workspaces',
Expand Down
36 changes: 35 additions & 1 deletion docs/explanation/features/V2_USER_SETTINGS_REDESIGN.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# V2 User Settings Redesign

**Implemented in version: 0.261.277** (layout); **0.261.278** (voice, audio and retention);
**0.261.279** (fact memory and Microsoft 365); **0.261.280** (guided tours and Latest Features)
**0.261.279** (fact memory and Microsoft 365); **0.261.280** (guided tours and Latest Features);
**0.261.296** (Open on Groups and Public rows)

## Overview

Expand All @@ -15,6 +16,8 @@
and personal retention (0.261.278).
3. Fact memory workbench and Microsoft 365 sharing and workflows (0.261.279).
4. Guided tours and the Latest Features shortcut (0.261.280).
5. Opening a group or public workspace from its row, as the classic page's **Manage** link
does (0.261.296).

Dependencies: the V2 React interface (`application/v2_ui`) and the user settings API
(`/api/user/settings`).
Expand Down Expand Up @@ -154,6 +157,28 @@
applies only to the version it was saved for, so the shortcut comes back after an upgrade.
`lib/latestFeaturesNav.ts` holds the logic both surfaces use.

### Groups and Public workspaces

`components/settings/WorkspaceListTab.tsx` renders both tabs from a `WorkspaceKind` in
`lib/workspaces.ts`, which holds what differs between the two: the list route, the setActive
route and its body key, and where a row opens.

- Each row shows the workspace name, the caller's role, any status other than active, an

Check warning on line 166 in docs/explanation/features/V2_USER_SETTINGS_REDESIGN.md

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
**Active** badge or a **Set active** button, and **Open**.
- **Set active** goes through the dedicated setActive route and then re-reads the list, because
the server resolves which workspace is active.
- **Open** (0.261.296) navigates to `WorkspaceKind.openPath(id)`, which is
`groupWorkspacePath(id)` (`/groups/<id>`) or `publicWorkspacePath(id)` (`/public/<id>`). Both
are reviewed same-origin builders in `scripts/check_xss_sinks.py`. Like the directories'
**Open**, the tab never activates anything itself; the page it lands on decides. A group page
activates the group it shows if it isn't active already, because a group's routes work in the
active group. A public workspace page loads by id and leaves the active public workspace alone.
- **Open** is disabled while a switch or a workspace-selection refresh is in progress, the same

Check warning on line 176 in docs/explanation/features/V2_USER_SETTINGS_REDESIGN.md

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
as **Set active**, so two activations never race. An id the path builder refuses is reported
in the tab's error line.
- Rows wrap the way the directory rows do. On a narrow screen the buttons move below the name,
and the role and status badges keep their width.

Check warning on line 180 in docs/explanation/features/V2_USER_SETTINGS_REDESIGN.md

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.

### Violations visibility

The Violations tab is always listed, as on the classic Profile page. When neither
Expand Down Expand Up @@ -191,6 +216,15 @@
accepted by the route, every tour step's anchor present, launchers and the Help and
guidance group, the bootstrap navigation entry, and a Node run of the shared tour and
Latest Features logic.
- `functional_tests/test_v2_settings_tabs.py`: each kind's Open path, the reviewed builders
and routes behind it, and an Open handler that never activates.
- `functional_tests/test_v2_group_workspace_context_logic.mjs` and

Check warning on line 221 in docs/explanation/features/V2_USER_SETTINGS_REDESIGN.md

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
`functional_tests/test_v2_public_workspace_context_logic.mjs`: the real `openPath` output,

Check warning on line 222 in docs/explanation/features/V2_USER_SETTINGS_REDESIGN.md

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
identifier encoding, and refused identifiers.
- `ui_tests/test_v2_settings_workspace_open.py`: against the built SPA, Open on a group that
isn't active (the group page activates it once), on the active group (no activation), and on
a public workspace (no activation), plus the row layout in both themes at desktop and phone
widths.
- Related fixes: [Feedback My Routes Filter Unpack Fix](../fixes/FEEDBACK_MY_FILTER_UNPACK_FIX.md)
and [Personal Retention Default Value Fix](../fixes/PERSONAL_RETENTION_DEFAULT_VALUE_FIX.md).

Expand Down
10 changes: 10 additions & 0 deletions docs/explanation/release_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

For feature-focused and fix-focused drill-downs by version, see [Features by Version](https://github.com/microsoft/simplechat/tree/main/docs/explanation/features) and [Fixes by Version](https://github.com/microsoft/simplechat/tree/main/docs/explanation/fixes).

### **(v0.261.296)**

#### User Interface Enhancements

* **Open Groups and Public Workspaces From User Settings**
* In V2 **User Settings**, every row on the **Groups** and **Public** tabs now has **Open** next to **Set active**, so you can go straight to a workspace you own or use instead of only making it active.
* **Open** goes to the workspace by its ID, like **Open** in the group and public directories. Opening a group also makes it your active group, because a group's pages work in the active group. Opening a public workspace leaves your active one unchanged.
* On narrow screens the row's buttons wrap below the workspace name, so the name and role badge are no longer squeezed.

Check warning on line 12 in docs/explanation/release_notes.md

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
* (Ref: `WorkspaceListTab.tsx`, `lib/workspaces.ts` `WorkspaceKind.openPath`, [V2 User Settings Redesign](features/V2_USER_SETTINGS_REDESIGN.md), `ui_tests/test_v2_settings_workspace_open.py`)

### **(v0.261.295)**

#### New Features
Expand Down
11 changes: 10 additions & 1 deletion docs/guides/update-profile-preferences.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
## In the new interface

In the new interface, open **User Settings** from the account menu. The left rail lists
**Preferences**, **Stats**, **Groups**, **Public workspaces**, **Feedback**, and
**Preferences**, **Stats**, **Groups**, **Public**, **Feedback**, and
**Violations**. To give the settings more room, collapse the rail to icons with the button
at its top. The rail stays collapsed until you expand it again, on any device.

Expand Down Expand Up @@ -103,6 +103,13 @@
you to confirm first. Chat reconnect opens Microsoft sign-in in a pop-up and keeps you on the
page; connecting for workflows signs in through the classic Profile page and returns there.

The **Groups** and **Public** tabs list the workspaces you can use, with your role in each.

Check warning on line 106 in docs/guides/update-profile-preferences.md

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
From version **0.261.296**, every row has **Open**, which takes you straight to that
workspace, next to **Set active**, which only changes where new work in this interface is
scoped and keeps you in settings. Opening a group also makes it your active group, because a
group's pages work in the active group. Opening a public workspace leaves your active public
workspace as it was.

**Violations** is always listed. If content safety is off for your application, the tab
says so instead of showing an empty list.

Expand Down Expand Up @@ -146,5 +153,7 @@
## Related

- [Manage notifications]({{ '/guides/manage-notifications/' | relative_url }})
- [Manage group workspaces]({{ '/guides/manage-group-workspaces/' | relative_url }})
- [Use public workspaces]({{ '/guides/use-public-workspaces/' | relative_url }})
- [Send feedback]({{ '/guides/send-feedback/' | relative_url }})
- [Safety settings]({{ '/admin/security/' | relative_url }})
13 changes: 12 additions & 1 deletion functional_tests/test_v2_group_workspace_context_logic.mjs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
// test_v2_group_workspace_context_logic.mjs
// Version: 0.261.281
// Version: 0.261.296
// Implemented in: 0.261.126
// Shared shell navigation and revalidation: 0.261.127
// Members section validation (M7B): 0.261.155
// Workspace navigation URL validation: 0.261.281
// User Settings Open path for a group row: 0.261.296
// Executes the real context API and stores with controlled HTTP ordering.

import assert from 'node:assert/strict';
Expand Down Expand Up @@ -439,6 +440,16 @@ try {
const invalid = GROUP_WORKSPACES.list(1, 25, '');
await assert.rejects(invalid, /invalid data/);
});
await run('a settings row opens its group page by encoded id without calling the server', async () => {
assert.equal(GROUP_WORKSPACES.openPath('group-a'), '/groups/group-a');
assert.equal(GROUP_WORKSPACES.openPath('group-a'), groupWorkspacePath('group-a'));
assert.equal(GROUP_WORKSPACES.openPath('group a&b'), '/groups/group%20a%26b');
for (const invalid of ['', '.', '..', 'group/a', 'group?a', 'group#a', ' group-a']) {
assert.throws(() => GROUP_WORKSPACES.openPath(invalid), /Invalid workspace identifier/);
}
// Open only navigates; the group page it lands on owns any activation.
assert.deepEqual(calls, []);
});
await run('revalidation keeps context while loading and retains drafts on transient failure', async () => {
const previous = await useGroupWorkspaceStore.getState().load('group-a');
const pending = deferred();
Expand Down
Loading
Loading