Skip to content
Open
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
9 changes: 8 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,18 @@ AtlasCode VSCode extension - Atlassian integration (Jira, Bitbucket, RovoDev)
- Unit: `*.test.ts` alongside source (`npm test`)
- React: `jest.react.config.ts` (`npm run test:react`)
- E2E: `e2e/` directory (`npm run test:e2e`)
- Create issue React coverage lives under `src/webviews/components/issue/create-issue-screen/`; compact Create Work Item reducer/UI coverage lives under `src/react/atlascode/create-work-item/`

### Architecture
- All Atlassian API calls → `src/atlclients/`
- Webview communication → `src/ipc/`
- Authentication → `authStore.ts`
- Jira create flows have two surfaces: full editor (`src/webviews/createIssueWebview.ts` + `CreateIssuePage.tsx`) and compact work item view (`src/work-items/create-work-item/` + `src/react/atlascode/create-work-item/`)
- Jira API error payloads often include actionable field errors in `errors`; preserve and render those instead of replacing them with generic Axios status messages

### Common Gotchas
- `npm ci` may fail if `package-lock.json` is out of sync with `package.json`; avoid committing lockfile churn unless dependency updates are the task
- Public npm installs may fail on private Atlassian packages such as `@atlassian/assets-workspace-host`; note this explicitly when tests or lint cannot run locally

## Environment Context
- `atlascode:bbyEnvironmentActive` = Boysenberry (internal Atlassian environment)
Expand All @@ -51,4 +58,4 @@ AtlasCode VSCode extension - Atlassian integration (Jira, Bitbucket, RovoDev)
3. Keep concise and actionable - focus on helping future agents avoid problems

**Include:** File patterns, integration points, auth requirements, testing strategies, error patterns
**Exclude:** Implementation details, temporary workarounds, user preferences
**Exclude:** Implementation details, temporary workarounds, user preferences
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Bug Fixes

- **Jira**: Show a clear create issue warning when Jira returns an empty create screen, and surface field-level Jira API errors instead of a generic 400 message.
- **RovoDev**: Hid stack traces, stderr, and log details from external users while preserving them for Atlassian users.
- **RovoDev (BBY)**: Fixed `ROVODEV_REBRAND_JCA` env var handling so the "Jira Coding Agent" rebrand works correctly in webviews.
- **Notifications**: Fixed `atlassianNotificationNotifier` to correctly flush all promise levels, resolving a test reliability issue.
Expand Down
30 changes: 28 additions & 2 deletions src/react/atlascode/create-work-item/createWorkItemWebview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import './CreateWorkItem.css';

import Form, { Field } from '@atlaskit/form';
import ChevronDownIcon from '@atlaskit/icon/core/chevron-down';
import SectionMessage from '@atlaskit/section-message';
import Select from '@atlaskit/select';
import React from 'react';
import { ConnectionTimeout } from 'src/util/time';
Expand All @@ -25,6 +26,7 @@ const emptyState: CreateFormState = {
selectedProjectId: undefined,
selectedIssueTypeId: undefined,
requiredFieldsForIssueType: [],
createScreenHasFields: true,
};

const CreateWorkItemWebview: React.FC = () => {
Expand Down Expand Up @@ -69,6 +71,9 @@ const CreateWorkItemWebview: React.FC = () => {
if (!state.summary || state.summary.trim().length === 0) {
errors['summary'] = 'EMPTY';
}
if (!state.createScreenHasFields) {
errors['_form'] = 'No create fields are configured for the selected work type.';
}
if (Object.keys(errors).length > 0) {
return errors;
}
Expand All @@ -83,7 +88,13 @@ const CreateWorkItemWebview: React.FC = () => {

return undefined;
},
[state.selectedIssueTypeId, state.selectedProjectId, state.selectedSiteId, state.summary],
[
state.createScreenHasFields,
state.selectedIssueTypeId,
state.selectedProjectId,
state.selectedSiteId,
state.summary,
],
);

const onMessageHandler = React.useCallback(
Expand Down Expand Up @@ -272,10 +283,20 @@ const CreateWorkItemWebview: React.FC = () => {
className="form-input"
placeholder="What needs to be done?"
type="text"
disabled={!state.createScreenHasFields}
onChange={handleUpdateSummary}
/>
)}
</Field>
{!state.createScreenHasFields && (
<SectionMessage appearance="warning" title="No create fields are configured">
<p>
This project&apos;s create screen has no fields configured for the selected work
type. Contact your Jira administrator to add the required fields to the create
screen.
</p>
</SectionMessage>
)}
<div className="form-actions-row">
<button type="button" className="more-options-link" onClick={handleOpenFullEditor}>
More options
Expand All @@ -286,11 +307,16 @@ const CreateWorkItemWebview: React.FC = () => {
Cancel
</button>
<div>
<button className="form-button button-primary" type="submit">
<button
className="form-button button-primary"
disabled={!state.createScreenHasFields}
type="submit"
>
Create
</button>
<button
className="form-button button-primary"
disabled={!state.createScreenHasFields}
style={{
borderLeft: '1px solid var(--vscode-button-foreground)',
padding: '6px',
Expand Down
54 changes: 54 additions & 0 deletions src/react/atlascode/create-work-item/utils.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { CreateWorkItemWebviewProviderMessageType } from 'src/work-items/create-work-item/messages/createWorkItemWebviewProviderMessages';
import { expansionCastTo } from 'testsutil';

import { CreateFormState, createReducer } from './utils';

describe('createReducer', () => {
const emptyState: CreateFormState = {
summary: '',
availableSites: [],
availableProjects: [],
availableIssueTypes: [],
selectedSiteId: undefined,
selectedProjectId: undefined,
selectedIssueTypeId: undefined,
requiredFieldsForIssueType: [],
createScreenHasFields: true,
};

it('stores whether the selected issue type has create screen fields on init', () => {
const result = createReducer(emptyState, {
type: CreateWorkItemWebviewProviderMessageType.InitFields,
payload: {
availableSites: [expansionCastTo({ id: 'site-1', name: 'Site 1', avatarUrl: '' })],
availableProjects: [
expansionCastTo({ id: 'project-1', key: 'TEST', name: 'Test Project', avatarUrls: {} }),
],
hasMoreProjects: false,
availableIssueTypes: [expansionCastTo({ id: 'issue-type-1', name: 'Task', iconUrl: '' })],
selectedSiteId: 'site-1',
selectedProjectId: 'project-1',
selectedIssueTypeId: 'issue-type-1',
requiredFields: [],
createScreenHasFields: false,
},
});

expect(result.createScreenHasFields).toBe(false);
});

it('updates whether the selected issue type has create screen fields', () => {
const result = createReducer(
{ ...emptyState, createScreenHasFields: false },
{
type: CreateWorkItemWebviewProviderMessageType.UpdatedSelectedIssueType,
payload: {
requiredFields: [],
createScreenHasFields: true,
},
},
);

expect(result.createScreenHasFields).toBe(true);
});
});
5 changes: 5 additions & 0 deletions src/react/atlascode/create-work-item/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface CreateFormState {
selectedIssueTypeId?: string;
hasMoreProjects?: boolean;
requiredFieldsForIssueType: CreateWorkItemViewRequiredField[];
createScreenHasFields: boolean;
}

export enum CreateFormActionType {
Expand Down Expand Up @@ -78,6 +79,7 @@ export function createReducer(state: CreateFormState, action: CreateFormAction):
selectedProjectId: action.payload.selectedProjectId || action.payload.availableProjects[0]?.id,
selectedIssueTypeId: action.payload.selectedIssueTypeId || action.payload.availableIssueTypes[0]?.id,
requiredFieldsForIssueType: action.payload.requiredFields,
createScreenHasFields: action.payload.createScreenHasFields,
};
}
case CreateFormActionType.SetSummary: {
Expand All @@ -103,6 +105,7 @@ export function createReducer(state: CreateFormState, action: CreateFormAction):
selectedProjectId: action.payload.selectedProjectId || action.payload.availableProjects[0]?.id,
selectedIssueTypeId: action.payload.selectedIssueTypeId || action.payload.availableIssueTypes[0]?.id,
requiredFieldsForIssueType: action.payload.requiredFields,
createScreenHasFields: action.payload.createScreenHasFields,
};
}
case CreateWorkItemWebviewProviderMessageType.UpdatedSelectedProject: {
Expand All @@ -115,12 +118,14 @@ export function createReducer(state: CreateFormState, action: CreateFormAction):
})),
selectedIssueTypeId: action.payload.selectedIssueTypeId || action.payload.availableIssueTypes[0]?.id,
requiredFieldsForIssueType: action.payload.requiredFields,
createScreenHasFields: action.payload.createScreenHasFields,
};
}
case CreateWorkItemWebviewProviderMessageType.UpdatedSelectedIssueType: {
return {
...state,
requiredFieldsForIssueType: action.payload.requiredFields,
createScreenHasFields: action.payload.createScreenHasFields,
};
}
case CreateFormActionType.SetSelectedField: {
Expand Down
22 changes: 22 additions & 0 deletions src/webviews/components/ErrorBanner.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,28 @@ describe('ErrorBanner', () => {
expect(container.textContent).toContain('Custom Error Title');
});

it('displays Jira field errors when errorMessages is omitted', () => {
const { container } = render(
<ErrorBanner
errorDetails={{
title: 'Error creating issue',
errors: {
summary: "Field 'summary' cannot be set. It is not on the appropriate screen, or unknown.",
},
}}
onDismissError={mockOnDismissError}
onRetry={mockOnRetry}
/>,
);

expect(container.textContent).toContain('Error creating issue');
expect(container.textContent).toContain('summary:');
expect(container.textContent).toContain(
"Field 'summary' cannot be set. It is not on the appropriate screen, or unknown.",
);
expect(container.textContent).not.toContain('{"summary"');
});

it('displays default title when not provided', () => {
const { container } = render(
<ErrorBanner errorDetails="Some error" onDismissError={mockOnDismissError} onRetry={mockOnRetry} />,
Expand Down
80 changes: 49 additions & 31 deletions src/webviews/components/ErrorBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type ErrorDetails =
| ErrorCollection
| ErrorWithMessages
| { message: string }
| { title?: string; errors: Record<string, string>; errorMessages?: string[] }
| { title?: string }
| undefined;

Expand Down Expand Up @@ -71,6 +72,33 @@ export default class ErrorBanner extends React.Component<ErrorBannerProps, { err
return '';
}

private getErrorCollectionMarkup(errorDetails: ErrorCollection) {
const errorMarkup: React.ReactNode[] = [];

Object.keys(errorDetails.errors).forEach((key) => {
errorMarkup.push(
<p className="force-wrap" key={`field-${key}`}>
<b>{key}:</b>
<span className="force-wrap" style={{ marginLeft: '5px' }}>
{errorDetails.errors[key]}
</span>
</p>,
);
});

errorDetails.errorMessages.forEach((msg, index) => {
errorMarkup.push(
<p className="force-wrap" key={`message-${index}`}>
<span className="force-wrap" style={{ marginLeft: '5px' }}>
{msg}
</span>
</p>,
);
});

return errorMarkup;
}

/**
* Determines if an error is retryable based on its content
* Auth/permission errors cannot be retried - user must re-authenticate
Expand Down Expand Up @@ -107,26 +135,7 @@ export default class ErrorBanner extends React.Component<ErrorBannerProps, { err
const errorDetails = this.state.errorDetails;

if (isErrorCollection(errorDetails)) {
Object.keys(errorDetails.errors).forEach((key) => {
errorMarkup.push(
<p className="force-wrap">
<b>{key}:</b>
<span className="force-wrap" style={{ marginLeft: '5px' }}>
{errorDetails.errors[key]}
</span>
</p>,
);
});

errorDetails.errorMessages.forEach((msg) => {
errorMarkup.push(
<p className="force-wrap">
<span className="force-wrap" style={{ marginLeft: '5px' }}>
{msg}
</span>
</p>,
);
});
errorMarkup.push(...this.getErrorCollectionMarkup(errorDetails));
} else if (isErrorWithMessages(errorDetails)) {
errorDetails.errorMessages.forEach((msg) => {
errorMarkup.push(
Expand All @@ -138,17 +147,26 @@ export default class ErrorBanner extends React.Component<ErrorBannerProps, { err
);
});
} else if (typeof errorDetails === 'object') {
Object.keys(errorDetails).forEach((key) => {
const value = errorDetails[key as keyof typeof errorDetails];
errorMarkup.push(
<p className="force-wrap">
<b>{key}:</b>
<span className="force-wrap" style={{ marginLeft: '5px' }}>
{JSON.stringify(value)}
</span>
</p>,
);
});
const maybeErrorCollection = {
errorMessages: [],
...errorDetails,
};

if (isErrorCollection(maybeErrorCollection)) {
errorMarkup.push(...this.getErrorCollectionMarkup(maybeErrorCollection));
} else {
Object.keys(errorDetails).forEach((key) => {
const value = errorDetails[key as keyof typeof errorDetails];
errorMarkup.push(
<p className="force-wrap">
<b>{key}:</b>
<span className="force-wrap" style={{ marginLeft: '5px' }}>
{JSON.stringify(value)}
</span>
</p>,
);
});
}
} else {
errorMarkup.push(<p className="force-wrap">{errorDetails}</p>);
}
Expand Down
Loading