зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1590493 - remove unnecessary calls to actorHasMethod and update supports state across the panel. r=mtigley
Differential Revision: https://phabricator.services.mozilla.com/D54569 --HG-- extra : moz-landing-system : lando
This commit is contained in:
Родитель
215a319521
Коммит
9eb54b9950
|
@ -47,29 +47,12 @@ class AccessibilityStartup {
|
|||
try {
|
||||
this._walker = await this._accessibility.getWalker();
|
||||
this._supports = {};
|
||||
// Only works with FF61+ targets
|
||||
this._supports.enableDisable = await this.target.actorHasMethod(
|
||||
"accessibility",
|
||||
"enable"
|
||||
);
|
||||
|
||||
if (this._supports.enableDisable) {
|
||||
[
|
||||
this._supports.relations,
|
||||
this._supports.snapshot,
|
||||
this._supports.audit,
|
||||
this._supports.hydration,
|
||||
this._supports.simulation,
|
||||
] = await Promise.all([
|
||||
this.target.actorHasMethod("accessible", "getRelations"),
|
||||
this.target.actorHasMethod("accessible", "snapshot"),
|
||||
this.target.actorHasMethod("accessible", "audit"),
|
||||
this.target.actorHasMethod("accessible", "hydrate"),
|
||||
[this._supports.simulation] = await Promise.all([
|
||||
// Added in Firefox 70.
|
||||
this.target.actorHasMethod("accessibility", "getSimulator"),
|
||||
]);
|
||||
|
||||
await this._accessibility.bootstrap();
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
|
|
|
@ -17,9 +17,6 @@ const { Provider } = require("devtools/client/shared/vendor/react-redux");
|
|||
|
||||
// Accessibility Panel
|
||||
const MainFrame = createFactory(require("./components/MainFrame"));
|
||||
const OldVersionDescription = createFactory(
|
||||
require("./components/Description").OldVersionDescription
|
||||
);
|
||||
|
||||
// Store
|
||||
const createStore = require("devtools/client/shared/redux/create-store");
|
||||
|
@ -79,12 +76,6 @@ AccessibilityView.prototype = {
|
|||
// Make sure state is reset every time accessibility panel is initialized.
|
||||
await this.store.dispatch(reset(front, supports));
|
||||
const container = document.getElementById("content");
|
||||
|
||||
if (!supports.enableDisable) {
|
||||
ReactDOM.render(OldVersionDescription(), container);
|
||||
return;
|
||||
}
|
||||
|
||||
const mainFrame = MainFrame({
|
||||
accessibility: front,
|
||||
accessibilityWalker: walker,
|
||||
|
@ -107,9 +98,9 @@ AccessibilityView.prototype = {
|
|||
window.emit(EVENTS.NEW_ACCESSIBLE_FRONT_HIGHLIGHTED);
|
||||
},
|
||||
|
||||
async selectNodeAccessible(walker, node, supports) {
|
||||
async selectNodeAccessible(walker, node) {
|
||||
let accessible = await walker.getAccessibleFor(node);
|
||||
if (accessible && supports.hydration) {
|
||||
if (accessible) {
|
||||
await accessible.hydrate();
|
||||
}
|
||||
|
||||
|
@ -124,7 +115,7 @@ AccessibilityView.prototype = {
|
|||
accessible = await walker.getAccessibleFor(child);
|
||||
// indexInParent property is only available with additional request
|
||||
// for data (hydration) about the accessible object.
|
||||
if (accessible && supports.hydration) {
|
||||
if (accessible) {
|
||||
await accessible.hydrate();
|
||||
}
|
||||
|
||||
|
|
|
@ -10,17 +10,16 @@ const { UPDATE_DETAILS } = require("../constants");
|
|||
*
|
||||
* @param {Object} dom walker front
|
||||
* @param {Object} accessible front
|
||||
* @param {Object} list of supported serverside features.
|
||||
*/
|
||||
exports.updateDetails = (domWalker, accessible, supports) => dispatch =>
|
||||
exports.updateDetails = (domWalker, accessible) => dispatch =>
|
||||
Promise.all([
|
||||
domWalker.getNodeFromActor(accessible.actorID, [
|
||||
"rawAccessible",
|
||||
"DOMNode",
|
||||
]),
|
||||
supports.relations ? accessible.getRelations() : [],
|
||||
supports.audit ? accessible.audit() : {},
|
||||
supports.hydration ? accessible.hydrate() : null,
|
||||
accessible.getRelations(),
|
||||
accessible.audit(),
|
||||
accessible.hydrate(),
|
||||
])
|
||||
.then(response => dispatch({ accessible, type: UPDATE_DETAILS, response }))
|
||||
.catch(error => dispatch({ accessible, type: UPDATE_DETAILS, error }));
|
||||
|
|
|
@ -80,10 +80,8 @@ class AccessibilityRow extends Component {
|
|||
static get propTypes() {
|
||||
return {
|
||||
...TreeRow.propTypes,
|
||||
hasContextMenu: PropTypes.bool.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
scrollContentNodeIntoView: PropTypes.bool.isRequired,
|
||||
supports: PropTypes.object,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -151,14 +149,13 @@ class AccessibilityRow extends Component {
|
|||
const {
|
||||
dispatch,
|
||||
member: { object },
|
||||
supports,
|
||||
} = this.props;
|
||||
if (!object.actorID) {
|
||||
return;
|
||||
}
|
||||
|
||||
const domWalker = (await object.targetFront.getFront("inspector")).walker;
|
||||
dispatch(updateDetails(domWalker, object, supports));
|
||||
dispatch(updateDetails(domWalker, object));
|
||||
window.emit(EVENTS.NEW_ACCESSIBLE_FRONT_SELECTED, object);
|
||||
}
|
||||
|
||||
|
@ -253,12 +250,6 @@ class AccessibilityRow extends Component {
|
|||
}
|
||||
|
||||
async printToJSON() {
|
||||
const { member, supports } = this.props;
|
||||
if (!supports.snapshot) {
|
||||
// Debugger server does not support Accessible actor snapshots.
|
||||
return;
|
||||
}
|
||||
|
||||
if (gTelemetry) {
|
||||
gTelemetry.keyedScalarAdd(
|
||||
TELEMETRY_ACCESSIBLE_CONTEXT_MENU_ITEM_ACTIVATED,
|
||||
|
@ -267,7 +258,7 @@ class AccessibilityRow extends Component {
|
|||
);
|
||||
}
|
||||
|
||||
const snapshot = await member.object.snapshot();
|
||||
const snapshot = await this.props.member.object.snapshot();
|
||||
openDocLink(
|
||||
`${JSON_URL_PREFIX}${encodeURIComponent(JSON.stringify(snapshot))}`
|
||||
);
|
||||
|
@ -282,9 +273,6 @@ class AccessibilityRow extends Component {
|
|||
}
|
||||
|
||||
const menu = new Menu({ id: "accessibility-row-contextmenu" });
|
||||
const { supports } = this.props;
|
||||
|
||||
if (supports.snapshot) {
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
id: "menu-printtojson",
|
||||
|
@ -292,7 +280,6 @@ class AccessibilityRow extends Component {
|
|||
click: () => this.printToJSON(),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
menu.popup(e.screenX, e.screenY, gToolbox.doc);
|
||||
|
||||
|
@ -309,7 +296,7 @@ class AccessibilityRow extends Component {
|
|||
const { member } = this.props;
|
||||
const props = {
|
||||
...this.props,
|
||||
onContextMenu: this.props.hasContextMenu && (e => this.onContextMenu(e)),
|
||||
onContextMenu: e => this.onContextMenu(e),
|
||||
onMouseOver: () => this.highlight(member.object),
|
||||
onMouseOut: () => this.unhighlight(member.object),
|
||||
key: `${member.path}-${member.active ? "active" : "inactive"}`,
|
||||
|
@ -325,9 +312,8 @@ class AccessibilityRow extends Component {
|
|||
}
|
||||
|
||||
const mapStateToProps = ({
|
||||
ui: { supports, [PREFS.SCROLL_INTO_VIEW]: scrollContentNodeIntoView },
|
||||
ui: { [PREFS.SCROLL_INTO_VIEW]: scrollContentNodeIntoView },
|
||||
}) => ({
|
||||
supports,
|
||||
scrollContentNodeIntoView,
|
||||
});
|
||||
|
||||
|
|
|
@ -11,8 +11,6 @@ const {
|
|||
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
|
||||
const { span } = require("devtools/client/shared/vendor/react-dom-factories");
|
||||
|
||||
const { connect } = require("devtools/client/shared/vendor/react-redux");
|
||||
|
||||
const Badges = createFactory(require("./Badges"));
|
||||
const AuditController = createFactory(require("./AuditController"));
|
||||
|
||||
|
@ -26,16 +24,10 @@ class AccessibilityRowValue extends Component {
|
|||
member: PropTypes.shape({
|
||||
object: PropTypes.object,
|
||||
}).isRequired,
|
||||
supports: PropTypes.object.isRequired,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
member,
|
||||
supports: { audit },
|
||||
} = this.props;
|
||||
|
||||
return span(
|
||||
{
|
||||
role: "presentation",
|
||||
|
@ -45,10 +37,9 @@ class AccessibilityRowValue extends Component {
|
|||
defaultRep: Grip,
|
||||
cropLimit: 50,
|
||||
}),
|
||||
audit &&
|
||||
AuditController(
|
||||
{
|
||||
accessibleFront: member.object,
|
||||
accessibleFront: this.props.member.object,
|
||||
},
|
||||
Badges()
|
||||
)
|
||||
|
@ -56,8 +47,4 @@ class AccessibilityRowValue extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = ({ ui: { supports } }) => {
|
||||
return { supports };
|
||||
};
|
||||
|
||||
module.exports = connect(mapStateToProps)(AccessibilityRowValue);
|
||||
module.exports = AccessibilityRowValue;
|
||||
|
|
|
@ -41,7 +41,6 @@ class AccessibilityTree extends Component {
|
|||
expanded: PropTypes.object,
|
||||
selected: PropTypes.string,
|
||||
highlighted: PropTypes.object,
|
||||
supports: PropTypes.object,
|
||||
filtered: PropTypes.bool,
|
||||
};
|
||||
}
|
||||
|
@ -167,21 +166,15 @@ class AccessibilityTree extends Component {
|
|||
expanded,
|
||||
selected,
|
||||
highlighted: highlightedItem,
|
||||
supports,
|
||||
accessibilityWalker,
|
||||
filtered,
|
||||
} = this.props;
|
||||
|
||||
// Historically, the first context menu item is snapshot function and it is available
|
||||
// for all accessible object.
|
||||
const hasContextMenu = supports.snapshot;
|
||||
|
||||
const renderRow = rowProps => {
|
||||
const { object } = rowProps.member;
|
||||
const highlighted = object === highlightedItem;
|
||||
return AccessibilityRow(
|
||||
Object.assign({}, rowProps, {
|
||||
hasContextMenu,
|
||||
highlighted,
|
||||
decorator: {
|
||||
getRowClass: function() {
|
||||
|
@ -217,9 +210,7 @@ class AccessibilityTree extends Component {
|
|||
|
||||
return true;
|
||||
},
|
||||
onContextMenuTree:
|
||||
hasContextMenu &&
|
||||
function(e) {
|
||||
onContextMenuTree: function(e) {
|
||||
// If context menu event is triggered on (or bubbled to) the TreeView, it was
|
||||
// done via keyboard. Open context menu for currently selected row.
|
||||
let row = this.getSelectedRow();
|
||||
|
@ -236,13 +227,12 @@ class AccessibilityTree extends Component {
|
|||
|
||||
const mapStateToProps = ({
|
||||
accessibles,
|
||||
ui: { expanded, selected, supports, highlighted },
|
||||
ui: { expanded, selected, highlighted },
|
||||
audit: { filters },
|
||||
}) => ({
|
||||
accessibles,
|
||||
expanded,
|
||||
selected,
|
||||
supports,
|
||||
highlighted,
|
||||
filtered: isFiltered(filters),
|
||||
});
|
||||
|
|
|
@ -112,7 +112,6 @@ class Accessible extends Component {
|
|||
labelledby: PropTypes.string.isRequired,
|
||||
parents: PropTypes.object,
|
||||
relations: PropTypes.object,
|
||||
supports: PropTypes.object,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -181,7 +180,7 @@ class Accessible extends Component {
|
|||
}
|
||||
|
||||
async update() {
|
||||
const { dispatch, accessibleFront, supports } = this.props;
|
||||
const { dispatch, accessibleFront } = this.props;
|
||||
if (!accessibleFront.actorID) {
|
||||
return;
|
||||
}
|
||||
|
@ -189,7 +188,7 @@ class Accessible extends Component {
|
|||
const domWalker = (await accessibleFront.targetFront.getFront("inspector"))
|
||||
.walker;
|
||||
|
||||
dispatch(updateDetails(domWalker, accessibleFront, supports));
|
||||
dispatch(updateDetails(domWalker, accessibleFront));
|
||||
}
|
||||
|
||||
setExpanded(item, isExpanded) {
|
||||
|
@ -540,13 +539,12 @@ const makeParentMap = items => {
|
|||
return map;
|
||||
};
|
||||
|
||||
const mapStateToProps = ({ details, ui }) => {
|
||||
const mapStateToProps = ({ details }) => {
|
||||
const {
|
||||
accessible: accessibleFront,
|
||||
DOMNode: nodeFront,
|
||||
relations,
|
||||
} = details;
|
||||
const { supports } = ui;
|
||||
if (!accessibleFront || !nodeFront) {
|
||||
return {};
|
||||
}
|
||||
|
@ -556,9 +554,7 @@ const mapStateToProps = ({ details, ui }) => {
|
|||
if (key === "DOMNode") {
|
||||
props.nodeFront = nodeFront;
|
||||
} else if (key === "relations") {
|
||||
if (supports.relations) {
|
||||
props.relations = relations;
|
||||
}
|
||||
} else {
|
||||
props[key] = accessibleFront[key];
|
||||
}
|
||||
|
@ -569,7 +565,7 @@ const mapStateToProps = ({ details, ui }) => {
|
|||
);
|
||||
const parents = makeParentMap(items);
|
||||
|
||||
return { accessibleFront, nodeFront, items, parents, relations, supports };
|
||||
return { accessibleFront, nodeFront, items, parents, relations };
|
||||
};
|
||||
|
||||
module.exports = connect(mapStateToProps)(Accessible);
|
||||
|
|
|
@ -93,10 +93,6 @@ class Checks extends Component {
|
|||
}
|
||||
|
||||
const mapStateToProps = ({ details, ui }) => {
|
||||
if (!ui.supports.audit) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const { audit } = details;
|
||||
if (!audit) {
|
||||
return {};
|
||||
|
|
|
@ -30,22 +30,6 @@ const {
|
|||
A11Y_SERVICE_ENABLED_COUNT,
|
||||
} = require("../constants");
|
||||
|
||||
class OldVersionDescription extends Component {
|
||||
render() {
|
||||
return div(
|
||||
{ className: "description" },
|
||||
p(
|
||||
{ className: "general" },
|
||||
img({
|
||||
src: "chrome://devtools/skin/images/accessibility.svg",
|
||||
alt: L10N.getStr("accessibility.logo"),
|
||||
}),
|
||||
L10N.getStr("accessibility.description.oldVersion")
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Landing UI for the accessibility panel when Accessibility features are
|
||||
* deactivated.
|
||||
|
@ -160,4 +144,3 @@ const mapStateToProps = ({ ui }) => ({
|
|||
|
||||
// Exports from this module
|
||||
exports.Description = connect(mapStateToProps)(Description);
|
||||
exports.OldVersionDescription = OldVersionDescription;
|
||||
|
|
|
@ -89,14 +89,8 @@ AccessibilityPanel.prototype = {
|
|||
this.panelWin.gToolbox = this._toolbox;
|
||||
|
||||
await this.startup.initAccessibility();
|
||||
if (this.supports.enableDisable) {
|
||||
this.picker = new Picker(this);
|
||||
}
|
||||
|
||||
if (this.supports.simulation) {
|
||||
this.simulator = await this.front.getSimulator();
|
||||
}
|
||||
|
||||
this.fluentBundles = await this.createFluentBundles();
|
||||
|
||||
this.updateA11YServiceDurationTimer();
|
||||
|
@ -202,12 +196,7 @@ AccessibilityPanel.prototype = {
|
|||
);
|
||||
}
|
||||
|
||||
this.postContentMessage(
|
||||
"selectNodeAccessible",
|
||||
this.walker,
|
||||
nodeFront,
|
||||
this.supports
|
||||
);
|
||||
this.postContentMessage("selectNodeAccessible", this.walker, nodeFront);
|
||||
},
|
||||
|
||||
highlightAccessible(accessibleFront) {
|
||||
|
|
|
@ -185,10 +185,8 @@ function onReset(state, { accessibility, supports }) {
|
|||
enabled,
|
||||
canBeDisabled,
|
||||
canBeEnabled,
|
||||
supports,
|
||||
};
|
||||
if (supports) {
|
||||
newState.supports = supports;
|
||||
}
|
||||
|
||||
return newState;
|
||||
}
|
||||
|
|
|
@ -43,9 +43,7 @@ window.onload = async function() {
|
|||
off: () => {},
|
||||
},
|
||||
},
|
||||
ui: {
|
||||
supports: {},
|
||||
},
|
||||
ui: { supports: {} }
|
||||
};
|
||||
|
||||
const mockStore = createStore((state, action) =>
|
||||
|
@ -53,28 +51,13 @@ window.onload = async function() {
|
|||
const provider = createElement(Provider, { store: mockStore }, a);
|
||||
const accessible = ReactDOM.render(provider, window.document.body);
|
||||
ok(accessible, "Should be able to mount Accessible instances");
|
||||
|
||||
info("Render accessible object when relations are not supported.");
|
||||
let relationsNode = document.getElementById("/relations");
|
||||
ok(!relationsNode, "Relations are not rendered when not supported.");
|
||||
|
||||
info("Render accessible object when relations are supported but are empty.");
|
||||
let state = {
|
||||
...mockState,
|
||||
ui: {
|
||||
supports: {
|
||||
relations: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
await mockStore.dispatch({ type: "update", ...state });
|
||||
relationsNode = document.getElementById("/relations");
|
||||
ok(relationsNode, "Relations are rendered when supported.");
|
||||
let arrow = relationsNode.querySelector(".arrow.theme-twisty");
|
||||
is(arrow.style.visibility, "hidden", "Relations are empty.");
|
||||
|
||||
info("Render accessible object with relations.");
|
||||
state = {
|
||||
const state = {
|
||||
details: {
|
||||
...mockState.details,
|
||||
relations: {
|
||||
|
@ -86,11 +69,6 @@ window.onload = async function() {
|
|||
},
|
||||
},
|
||||
},
|
||||
ui: {
|
||||
supports: {
|
||||
relations: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
await mockStore.dispatch({ type: "update", ...state });
|
||||
relationsNode = document.getElementById("/relations");
|
||||
|
|
|
@ -90,25 +90,16 @@ window.onload = async function() {
|
|||
hasContextMenu: true,
|
||||
};
|
||||
|
||||
const mockProps = {
|
||||
...defaultProps,
|
||||
hasContextMenu: null,
|
||||
};
|
||||
|
||||
const auditState = { audit: { filters: { [FILTERS.CONTRAST]: false }}};
|
||||
|
||||
const defaultState = {
|
||||
ui: { supports: { snapshot: true }},
|
||||
...auditState,
|
||||
};
|
||||
const mockState = {
|
||||
ui: { supports: {}},
|
||||
ui: { supports: {} },
|
||||
...auditState,
|
||||
};
|
||||
|
||||
info("Check contextmenu default behaviour.");
|
||||
renderAccessibilityRow(defaultProps, defaultState);
|
||||
let row = document.getElementById(ROW_ID);
|
||||
const row = document.getElementById(ROW_ID);
|
||||
|
||||
info("Get topmost document where the context meny will be rendered");
|
||||
const menuDoc = document.defaultView.windowRoot.ownerGlobal.document;
|
||||
|
@ -144,23 +135,6 @@ window.onload = async function() {
|
|||
browserWindow.openWebLinkIn = defaultOpenWebLinkIn;
|
||||
menu.remove();
|
||||
});
|
||||
|
||||
info("Check accessibility row when context menu is not supported.");
|
||||
renderAccessibilityRow(defaultProps, mockState);
|
||||
row = document.getElementById(ROW_ID);
|
||||
|
||||
info("Check contextmenu listener is not called when context menu is not supported.");
|
||||
Simulate.contextMenu(row);
|
||||
let menu = menuDoc.getElementById("accessibility-row-contextmenu");
|
||||
ok(!menu, "contextmenu event handler was never called.");
|
||||
|
||||
info("Check accessibility row when no context menu is available.");
|
||||
renderAccessibilityRow(mockProps, defaultState);
|
||||
row = document.getElementById(ROW_ID);
|
||||
|
||||
Simulate.contextMenu(row);
|
||||
menu = menuDoc.getElementById("accessibility-row-contextmenu");
|
||||
ok(!menu, "contextmenu event handler was never called.");
|
||||
} catch (e) {
|
||||
ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
|
||||
} finally {
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AccessibilityRowValue component: audit not supported 1`] = `"<span role=\\"presentation\\"><span class=\\"objectBox objectBox-undefined\\">undefined</span></span>"`;
|
||||
|
||||
exports[`AccessibilityRowValue component: basic render 1`] = `"<span role=\\"presentation\\"><span class=\\"objectBox objectBox-undefined\\">undefined</span></span>"`;
|
||||
|
|
|
@ -21,40 +21,13 @@ const {
|
|||
} = require("devtools/client/shared/components/reps/reps");
|
||||
const AuditController = require("devtools/client/accessibility/components/AuditController");
|
||||
|
||||
const ConnectedAccessibilityRowValueClass = require("devtools/client/accessibility/components/AccessibilityRowValue");
|
||||
const AccessibilityRowValueClass =
|
||||
ConnectedAccessibilityRowValueClass.WrappedComponent;
|
||||
const AccessibilityRowValue = createFactory(
|
||||
ConnectedAccessibilityRowValueClass
|
||||
);
|
||||
const AccessibilityRowValueClass = require("devtools/client/accessibility/components/AccessibilityRowValue");
|
||||
const AccessibilityRowValue = createFactory(AccessibilityRowValueClass);
|
||||
|
||||
describe("AccessibilityRowValue component:", () => {
|
||||
it("audit not supported", () => {
|
||||
const store = setupStore({
|
||||
preloadedState: { ui: { supports: {} } },
|
||||
});
|
||||
const wrapper = mount(
|
||||
Provider(
|
||||
{ store },
|
||||
AccessibilityRowValue({
|
||||
member: { object: mockAccessible() },
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
expect(wrapper.html()).toMatchSnapshot();
|
||||
const rowValue = wrapper.find(AccessibilityRowValueClass);
|
||||
expect(rowValue.children().length).toBe(1);
|
||||
const container = rowValue.childAt(0);
|
||||
expect(container.type()).toBe("span");
|
||||
expect(container.prop("role")).toBe("presentation");
|
||||
expect(container.children().length).toBe(1);
|
||||
expect(container.childAt(0).type()).toBe(Rep);
|
||||
});
|
||||
|
||||
it("basic render", () => {
|
||||
const store = setupStore({
|
||||
preloadedState: { ui: { supports: { audit: true } } },
|
||||
preloadedState: { ui: { supports: {} } },
|
||||
});
|
||||
const wrapper = mount(
|
||||
Provider(
|
||||
|
|
Загрузка…
Ссылка в новой задаче