瀏覽代碼

SONAR-12718 User can get a permalink to the hotspot

tags/8.2.0.32929
Philippe Perrin 4 年之前
父節點
當前提交
42f203d478
共有 15 個檔案被更改,包括 279 行新增32 行删除
  1. 1
    2
      server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx
  2. 4
    3
      server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx
  3. 2
    1
      server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsAppRenderer-test.tsx
  4. 23
    1
      server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap
  5. 46
    1
      server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsAppRenderer-test.tsx.snap
  6. 4
    2
      server/sonar-web/src/main/js/apps/security-hotspots/components/FilterBar.tsx
  7. 5
    3
      server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewer.tsx
  8. 20
    2
      server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerRenderer.tsx
  9. 9
    5
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/FilterBar-test.tsx
  10. 2
    0
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewer-test.tsx
  11. 4
    1
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewerRenderer-test.tsx
  12. 46
    0
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewer-test.tsx.snap
  13. 110
    10
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewerRenderer-test.tsx.snap
  14. 2
    1
      server/sonar-web/src/main/js/helpers/testMocks.ts
  15. 1
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 1
- 2
server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx 查看文件

@@ -30,7 +30,6 @@ import { getBranchLikeQuery, isPullRequest, isSameBranchLike } from '../../helpe
import { getStandards } from '../../helpers/security-standard';
import { isLoggedIn } from '../../helpers/users';
import { BranchLike } from '../../types/branch-like';
import { ComponentQualifier } from '../../types/component';
import {
HotspotFilters,
HotspotResolution,
@@ -330,11 +329,11 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> {
return (
<SecurityHotspotsAppRenderer
branchLike={branchLike}
component={component}
filters={filters}
hotspots={hotspots}
hotspotsReviewedMeasure={hotspotsReviewedMeasure}
hotspotsTotal={hotspotsTotal}
isProject={component.qualifier === ComponentQualifier.Project}
isStaticListOfHotspots={Boolean(hotspotKeys && hotspotKeys.length > 0)}
loading={loading}
loadingMeasure={loadingMeasure}

+ 4
- 3
server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx 查看文件

@@ -35,11 +35,11 @@ import './styles.css';

export interface SecurityHotspotsAppRendererProps {
branchLike?: BranchLike;
component: T.Component;
filters: HotspotFilters;
hotspots: RawHotspot[];
hotspotsReviewedMeasure?: string;
hotspotsTotal?: number;
isProject: boolean;
isStaticListOfHotspots: boolean;
loading: boolean;
loadingMeasure: boolean;
@@ -56,10 +56,10 @@ export interface SecurityHotspotsAppRendererProps {
export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRendererProps) {
const {
branchLike,
component,
hotspots,
hotspotsReviewedMeasure,
hotspotsTotal,
isProject,
isStaticListOfHotspots,
loading,
loadingMeasure,
@@ -72,9 +72,9 @@ export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRe
return (
<div id="security_hotspots">
<FilterBar
component={component}
filters={filters}
hotspotsReviewedMeasure={hotspotsReviewedMeasure}
isProject={isProject}
isStaticListOfHotspots={isStaticListOfHotspots}
loadingMeasure={loadingMeasure}
onBranch={isBranch(branchLike)}
@@ -120,6 +120,7 @@ export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRe
<div className="main">
<HotspotViewer
branchLike={branchLike}
component={component}
hotspotKey={selectedHotspot.key}
onUpdateHotspot={props.onUpdateHotspot}
securityCategories={securityCategories}

+ 2
- 1
server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsAppRenderer-test.tsx 查看文件

@@ -21,6 +21,7 @@ import { shallow } from 'enzyme';
import * as React from 'react';
import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
import { mockRawHotspot } from '../../../helpers/mocks/security-hotspots';
import { mockComponent } from '../../../helpers/testMocks';
import { HotspotStatusFilter } from '../../../types/security-hotspots';
import FilterBar from '../components/FilterBar';
import SecurityHotspotsAppRenderer, {
@@ -72,13 +73,13 @@ it('should properly propagate the "show all" call', () => {
function shallowRender(props: Partial<SecurityHotspotsAppRendererProps> = {}) {
return shallow(
<SecurityHotspotsAppRenderer
component={mockComponent()}
filters={{
assignedToMe: false,
sinceLeakPeriod: false,
status: HotspotStatusFilter.TO_REVIEW
}}
hotspots={[]}
isProject={true}
isStaticListOfHotspots={true}
loading={false}
loadingMeasure={false}

+ 23
- 1
server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap 查看文件

@@ -10,6 +10,29 @@ exports[`should render correctly 1`] = `
"name": "branch-6.7",
}
}
component={
Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "TRK",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
}
}
filters={
Object {
"assignedToMe": false,
@@ -18,7 +41,6 @@ exports[`should render correctly 1`] = `
}
}
hotspots={Array []}
isProject={true}
isStaticListOfHotspots={false}
loading={true}
loadingMeasure={true}

+ 46
- 1
server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsAppRenderer-test.tsx.snap 查看文件

@@ -5,6 +5,29 @@ exports[`should render correctly 1`] = `
id="security_hotspots"
>
<Connect(withCurrentUser(FilterBar))
component={
Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "TRK",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
}
}
filters={
Object {
"assignedToMe": false,
@@ -12,7 +35,6 @@ exports[`should render correctly 1`] = `
"status": "TO_REVIEW",
}
}
isProject={true}
isStaticListOfHotspots={true}
loadingMeasure={false}
onBranch={false}
@@ -146,6 +168,29 @@ exports[`should render correctly with hotspots 2`] = `
className="main"
>
<HotspotViewer
component={
Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "TRK",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
}
}
hotspotKey="h2"
onUpdateHotspot={[MockFunction]}
securityCategories={Object {}}

+ 4
- 2
server/sonar-web/src/main/js/apps/security-hotspots/components/FilterBar.tsx 查看文件

@@ -27,13 +27,14 @@ import { withCurrentUser } from '../../../components/hoc/withCurrentUser';
import Measure from '../../../components/measure/Measure';
import CoverageRating from '../../../components/ui/CoverageRating';
import { isLoggedIn } from '../../../helpers/users';
import { ComponentQualifier } from '../../../types/component';
import { HotspotFilters, HotspotStatusFilter } from '../../../types/security-hotspots';

export interface FilterBarProps {
currentUser: T.CurrentUser;
component: T.Component;
filters: HotspotFilters;
hotspotsReviewedMeasure?: string;
isProject: boolean;
isStaticListOfHotspots: boolean;
loadingMeasure: boolean;
onBranch: boolean;
@@ -65,13 +66,14 @@ const assigneeFilterOptions = [
export function FilterBar(props: FilterBarProps) {
const {
currentUser,
component,
filters,
hotspotsReviewedMeasure,
isProject,
isStaticListOfHotspots,
loadingMeasure,
onBranch
} = props;
const isProject = component.qualifier === ComponentQualifier.Project;

return (
<div className="filter-bar display-flex-center">

+ 5
- 3
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewer.tsx 查看文件

@@ -26,6 +26,7 @@ import HotspotViewerRenderer from './HotspotViewerRenderer';

interface Props {
branchLike?: BranchLike;
component: T.Component;
hotspotKey: string;
onUpdateHotspot: (hotspotKey: string) => Promise<void>;
securityCategories: T.StandardSecurityCategories;
@@ -55,7 +56,7 @@ export default class HotspotViewer extends React.PureComponent<Props, State> {
this.mounted = false;
}

fetchHotspot() {
fetchHotspot = () => {
this.setState({ loading: true });
return getSecurityHotspotDetails(this.props.hotspotKey)
.then(hotspot => {
@@ -65,7 +66,7 @@ export default class HotspotViewer extends React.PureComponent<Props, State> {
return hotspot;
})
.catch(() => this.mounted && this.setState({ loading: false }));
}
};

handleHotspotUpdate = () => {
return this.fetchHotspot().then((hotspot?: Hotspot) => {
@@ -76,12 +77,13 @@ export default class HotspotViewer extends React.PureComponent<Props, State> {
};

render() {
const { branchLike, securityCategories } = this.props;
const { branchLike, component, securityCategories } = this.props;
const { hotspot, loading } = this.state;

return (
<HotspotViewerRenderer
branchLike={branchLike}
component={component}
hotspot={hotspot}
loading={loading}
onUpdateHotspot={this.handleHotspotUpdate}

+ 20
- 2
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerRenderer.tsx 查看文件

@@ -18,8 +18,13 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { ClipboardButton } from 'sonar-ui-common/components/controls/clipboard';
import LinkIcon from 'sonar-ui-common/components/icons/LinkIcon';
import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { getPathUrlAsString } from 'sonar-ui-common/helpers/urls';
import { getBranchLikeQuery } from '../../../helpers/branch-like';
import { getComponentSecurityHotspotsUrl } from '../../../helpers/urls';
import { BranchLike } from '../../../types/branch-like';
import { Hotspot } from '../../../types/security-hotspots';
import Assignee from './assignee/Assignee';
@@ -29,6 +34,7 @@ import Status from './status/Status';

export interface HotspotViewerRendererProps {
branchLike?: BranchLike;
component: T.Component;
hotspot?: Hotspot;
loading: boolean;
onUpdateHotspot: () => Promise<void>;
@@ -36,7 +42,15 @@ export interface HotspotViewerRendererProps {
}

export default function HotspotViewerRenderer(props: HotspotViewerRendererProps) {
const { branchLike, hotspot, loading, securityCategories } = props;
const { branchLike, component, hotspot, loading, securityCategories } = props;

const permalink = getPathUrlAsString(
getComponentSecurityHotspotsUrl(component.key, {
...getBranchLikeQuery(branchLike),
hotspots: hotspot?.key
}),
false
);

return (
<DeferredSpinner loading={loading}>
@@ -44,7 +58,11 @@ export default function HotspotViewerRenderer(props: HotspotViewerRendererProps)
<div className="big-padded">
<div className="big-spacer-bottom">
<div className="display-flex-space-between">
<h1>{hotspot.message}</h1>
<strong className="big">{hotspot.message}</strong>
<ClipboardButton copyValue={permalink}>
<LinkIcon className="spacer-right" />
<span>{translate('hotspots.get_permalink')}</span>
</ClipboardButton>
</div>
<div className="text-muted">
<span>{translate('category')}:</span>

+ 9
- 5
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/FilterBar-test.tsx 查看文件

@@ -21,7 +21,8 @@ import { shallow } from 'enzyme';
import * as React from 'react';
import RadioToggle from 'sonar-ui-common/components/controls/RadioToggle';
import Select from 'sonar-ui-common/components/controls/Select';
import { mockCurrentUser, mockLoggedInUser } from '../../../../helpers/testMocks';
import { mockComponent, mockCurrentUser, mockLoggedInUser } from '../../../../helpers/testMocks';
import { ComponentQualifier } from '../../../../types/component';
import { HotspotStatusFilter } from '../../../../types/security-hotspots';
import { AssigneeFilterOption, FilterBar, FilterBarProps } from '../FilterBar';

@@ -32,9 +33,12 @@ it('should render correctly', () => {
expect(shallowRender({ hotspotsReviewedMeasure: '23.30' })).toMatchSnapshot(
'with hotspots reviewed measure'
);
expect(shallowRender({ currentUser: mockLoggedInUser(), isProject: false })).toMatchSnapshot(
'non-project'
);
expect(
shallowRender({
currentUser: mockLoggedInUser(),
component: mockComponent({ qualifier: ComponentQualifier.Application })
})
).toMatchSnapshot('non-project');
});

it('should render correctly when the list of hotspot is static', () => {
@@ -101,13 +105,13 @@ it('should trigger onChange for leak period', () => {
function shallowRender(props: Partial<FilterBarProps> = {}) {
return shallow(
<FilterBar
component={mockComponent()}
currentUser={mockCurrentUser()}
filters={{
assignedToMe: false,
sinceLeakPeriod: false,
status: HotspotStatusFilter.TO_REVIEW
}}
isProject={true}
isStaticListOfHotspots={false}
loadingMeasure={false}
onBranch={true}

+ 2
- 0
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewer-test.tsx 查看文件

@@ -21,6 +21,7 @@ import { shallow } from 'enzyme';
import * as React from 'react';
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
import { getSecurityHotspotDetails } from '../../../../api/security-hotspots';
import { mockComponent } from '../../../../helpers/testMocks';
import HotspotViewer from '../HotspotViewer';

const hotspotKey = 'hotspot-key';
@@ -48,6 +49,7 @@ it('should render correctly', async () => {
function shallowRender(props?: Partial<HotspotViewer['props']>) {
return shallow<HotspotViewer>(
<HotspotViewer
component={mockComponent()}
hotspotKey={hotspotKey}
onUpdateHotspot={jest.fn()}
securityCategories={{ cat1: { title: 'cat1' } }}

+ 4
- 1
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewerRenderer-test.tsx 查看文件

@@ -19,8 +19,9 @@
*/
import { shallow } from 'enzyme';
import * as React from 'react';
import { mockBranch } from '../../../../helpers/mocks/branch-like';
import { mockHotspot } from '../../../../helpers/mocks/security-hotspots';
import { mockUser } from '../../../../helpers/testMocks';
import { mockComponent, mockUser } from '../../../../helpers/testMocks';
import HotspotViewerRenderer, { HotspotViewerRendererProps } from '../HotspotViewerRenderer';

it('should render correctly', () => {
@@ -46,6 +47,8 @@ it('should render correctly', () => {
function shallowRender(props?: Partial<HotspotViewerRendererProps>) {
return shallow(
<HotspotViewerRenderer
branchLike={mockBranch()}
component={mockComponent()}
hotspot={mockHotspot()}
loading={false}
onUpdateHotspot={jest.fn()}

+ 46
- 0
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewer-test.tsx.snap 查看文件

@@ -2,6 +2,29 @@

exports[`should render correctly 1`] = `
<HotspotViewerRenderer
component={
Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "TRK",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
}
}
loading={true}
onUpdateHotspot={[Function]}
securityCategories={
@@ -16,6 +39,29 @@ exports[`should render correctly 1`] = `

exports[`should render correctly 2`] = `
<HotspotViewerRenderer
component={
Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "TRK",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
}
}
hotspot={
Object {
"id": "I am a detailled hotspot",

+ 110
- 10
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewerRenderer-test.tsx.snap 查看文件

@@ -14,9 +14,21 @@ exports[`should render correctly 1`] = `
<div
className="display-flex-space-between"
>
<h1>
<strong
className="big"
>
'3' is a magic number.
</h1>
</strong>
<ClipboardButton
copyValue="http://localhost/security_hotspots?id=my-project&branch=branch-6.7&hotspots=01fc972e-2a3c-433e-bcae-0bd7f88f5123"
>
<LinkIcon
className="spacer-right"
/>
<span>
hotspots.get_permalink
</span>
</ClipboardButton>
</div>
<div
className="text-muted"
@@ -241,6 +253,14 @@ exports[`should render correctly 1`] = `
/>
</div>
<HotspotSnippetContainer
branchLike={
Object {
"analysisDate": "2018-01-01",
"excludedFromPurge": true,
"isMain": false,
"name": "branch-6.7",
}
}
hotspot={
Object {
"assignee": "assignee",
@@ -461,9 +481,21 @@ exports[`should render correctly: anonymous user 1`] = `
<div
className="display-flex-space-between"
>
<h1>
<strong
className="big"
>
'3' is a magic number.
</h1>
</strong>
<ClipboardButton
copyValue="http://localhost/security_hotspots?id=my-project&branch=branch-6.7&hotspots=01fc972e-2a3c-433e-bcae-0bd7f88f5123"
>
<LinkIcon
className="spacer-right"
/>
<span>
hotspots.get_permalink
</span>
</ClipboardButton>
</div>
<div
className="text-muted"
@@ -688,6 +720,14 @@ exports[`should render correctly: anonymous user 1`] = `
/>
</div>
<HotspotSnippetContainer
branchLike={
Object {
"analysisDate": "2018-01-01",
"excludedFromPurge": true,
"isMain": false,
"name": "branch-6.7",
}
}
hotspot={
Object {
"assignee": "assignee",
@@ -908,9 +948,21 @@ exports[`should render correctly: assignee without name 1`] = `
<div
className="display-flex-space-between"
>
<h1>
<strong
className="big"
>
'3' is a magic number.
</h1>
</strong>
<ClipboardButton
copyValue="http://localhost/security_hotspots?id=my-project&branch=branch-6.7&hotspots=01fc972e-2a3c-433e-bcae-0bd7f88f5123"
>
<LinkIcon
className="spacer-right"
/>
<span>
hotspots.get_permalink
</span>
</ClipboardButton>
</div>
<div
className="text-muted"
@@ -1135,6 +1187,14 @@ exports[`should render correctly: assignee without name 1`] = `
/>
</div>
<HotspotSnippetContainer
branchLike={
Object {
"analysisDate": "2018-01-01",
"excludedFromPurge": true,
"isMain": false,
"name": "branch-6.7",
}
}
hotspot={
Object {
"assignee": "assignee",
@@ -1355,9 +1415,21 @@ exports[`should render correctly: deleted assignee 1`] = `
<div
className="display-flex-space-between"
>
<h1>
<strong
className="big"
>
'3' is a magic number.
</h1>
</strong>
<ClipboardButton
copyValue="http://localhost/security_hotspots?id=my-project&branch=branch-6.7&hotspots=01fc972e-2a3c-433e-bcae-0bd7f88f5123"
>
<LinkIcon
className="spacer-right"
/>
<span>
hotspots.get_permalink
</span>
</ClipboardButton>
</div>
<div
className="text-muted"
@@ -1582,6 +1654,14 @@ exports[`should render correctly: deleted assignee 1`] = `
/>
</div>
<HotspotSnippetContainer
branchLike={
Object {
"analysisDate": "2018-01-01",
"excludedFromPurge": true,
"isMain": false,
"name": "branch-6.7",
}
}
hotspot={
Object {
"assignee": "assignee",
@@ -1809,9 +1889,21 @@ exports[`should render correctly: unassigned 1`] = `
<div
className="display-flex-space-between"
>
<h1>
<strong
className="big"
>
'3' is a magic number.
</h1>
</strong>
<ClipboardButton
copyValue="http://localhost/security_hotspots?id=my-project&branch=branch-6.7&hotspots=01fc972e-2a3c-433e-bcae-0bd7f88f5123"
>
<LinkIcon
className="spacer-right"
/>
<span>
hotspots.get_permalink
</span>
</ClipboardButton>
</div>
<div
className="text-muted"
@@ -2036,6 +2128,14 @@ exports[`should render correctly: unassigned 1`] = `
/>
</div>
<HotspotSnippetContainer
branchLike={
Object {
"analysisDate": "2018-01-01",
"excludedFromPurge": true,
"isMain": false,
"name": "branch-6.7",
}
}
hotspot={
Object {
"assignee": undefined,

+ 2
- 1
server/sonar-web/src/main/js/helpers/testMocks.ts 查看文件

@@ -23,6 +23,7 @@ import { InjectedRouter } from 'react-router';
import { createStore, Store } from 'redux';
import { DocumentationEntry } from '../apps/documentation/utils';
import { Exporter, Profile } from '../apps/quality-profiles/types';
import { ComponentQualifier } from '../types/component';

export function mockAlmApplication(overrides: Partial<T.AlmApplication> = {}): T.AlmApplication {
return {
@@ -277,7 +278,7 @@ export function mockComponent(overrides: Partial<T.Component> = {}): T.Component
key: 'my-project',
name: 'MyProject',
organization: 'foo',
qualifier: 'TRK',
qualifier: ComponentQualifier.Project,
qualityGate: { isDefault: true, key: '30', name: 'Sonar way' },
qualityProfiles: [
{

+ 1
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties 查看文件

@@ -685,6 +685,7 @@ hotspots.status_option.FIXED=Fixed
hotspots.status_option.FIXED.description=The code has been modified to follow recommended secure coding practices.
hotspots.status_option.SAFE=Safe
hotspots.status_option.SAFE.description=The code is not at risk and doesn't need to be modified.
hotspots.get_permalink=Get Permalink

hotspot.filters.title=Filters
hotspot.filters.assignee.assigned_to_me=Assigned to me

Loading…
取消
儲存