Эта страница создана, чтобы собрать воедино информацию по конфигуратору Yougile и иметь базу данных для обучения ИИ. Она будет пополняться по мере необходимости в новых переменных, методах или при появлении нового кода.
Как планирую использовать этот конфигуратор — наполню его, отдам все Claude AI, чтобы иметь своего разработчика по скриптам Yougile.
Я просто хочу, чтобы это стало доступно и другим пользователям. Потому что собрать документацию в одном месте, это сложно. Потрачен не один час, чтобы все выковырять и оформить.
Разработчики Yougile, если вы это читаете — пожалуйста выгрузите все методы и переменные API конфигуратора, чтобы её можно было просто скопировать одним кликом или скачать файлом.
Оглавление статьи
Что такое конфигуратор?
Конфигуратор — это среда, в которой можно создавать javascript-код, который будет дополнять функциональность системы Yougile.
Полезные ссылки
- https://ru.yougile.com/team/api-editor — страница конфгуратора. Нужно быть авторизованным. Откроются скрипты именно вашего проекта.
- https://www.youtube.com/watch?v=UOKxyTRv0Bo — 10-минутное видео с примерами реализации скриптов, решающих конкретные задачи.
- https://ru.yougile.com/media/docs/yougile-api-manual.pdf — короткий мануал в несколько станиц, который может помочь вам лучше понять как устроено API YouGile.
Навигатор по API
Notifier
// Notifier.error(message)
// Показывает нотификацию ошибки.
Notifier.error('Task was added successfully');
// Notifier.warn(message)
// Показывает нотификацию предупреждения.
Notifier.warn('This task is readonly!');
// Notifier.success(message)
// Показывает нотификацию успешно выполненного действия.
Notifier.success('Task was added successfully');
//Notifier.show(title, message, onClick)
//Показывает браузерную нотификацию.
Notifier.show('New message', 'Hi, how’s it going?');
// Notifier.playSound(soundNumber)
// Проигрывает звук. Доступны 7 вариантов звуков.
// Звук по умолчанию, как в чатах
Notifier.playSound();
// Другой звук (доступно 7 вариантов)
Notifier.playSound(2);
Current
// Current.user
// Текущий пользователь (см. “user variable”).
if (Current.user.name === 'John') {
console.log('Hi John!');
}
// Current.project
// Открытый проект (см. “item variable”). Только для чтения.
var project = Current.project;
console.log('Открытый проект:', project);
// Current.board
// Открытая доска (см. “item variable”). Только для чтения.
var board = Current.board;
console.log('Открытая доска:', board);
// Current.chat
// Открытый чат задачи (см. “item variable”). Только для чтения.
var chat = Current.chat;
console.log('Открытый чат задачи:', chat);
// Current.company
// Возвращает компанию, в которой запускается скрипт.
var company = Current.company;
console.log('Company is ' + company.name);
// Current.onProjectChange(oldProject, newProject)
// Эта функция вызывается, когда пользователь переходит в другой проект.
Current.onProjectChange = function (oldProject, newProject) {
if (newProject) {
console.log('Navigated to ' + newProject.name);
} else {
console.warn('Exited project');
}
};
// Current.onBoardChange(oldBoard, newBoard)
// Эта функция вызывается, когда пользователь переходит в новую доску.
Current.onBoardChange = function (oldBoard, newBoard) {
console.log('Board ' + newBoard.name + ' opened!');
};
// Current.onChatChange(oldChat, newChat)
// Эта функция вызывается, когда открывается чат по другой задаче.
Current.onChatChange = function (oldChat, newChat) {
console.log('Opened chat for ' + newChat.name + ' task!');
};
Items
Позволяет манипулировать всеми объектами внутри YouGile (задачи, колонки, доски, проекты) и описывает поведение по умолчанию для этих объектов. Проекты, борды, колонки и задачи структурированы в некую иерархичную систему, похожую на файловую систему компьютера (смотри вкладку «Навигатор по объектам»). «Items» позволяет получить доступ и управлять ими на этот манер. И когда вы обращаетесь к какой-либо задаче или доске через Items, вы получаете переменную типа item (см. «item variable»).
Items.get (id)
Получает объект YouGile по его id. Объекты YouGile: компания, проекты, доски, колонки, задачи. Вы можете найти id объектов во вкладке «Навигатор по объектам».
// Getting a task and assigning it to variable “t” for later use
var t = Items.get('bb12a559-81ba-40ad-adda-81f2d017e71b');
// Appending “!” to the task’s name
t.name = t.name + '!';
Items.onAdd (newObject)
Эта функция вызывается сразу после добавления нового объекта в систему (задачи, колонки, доски, проекта).
Items.onAdd = function (obj) {
if (obj.type === 'Task') { // only when a task was added
// Changing the name of the new task
obj.name = obj.name + ' ...';
}
};
Items.onBeforeAdd (location, name)
Эта функция вызывается, перед добавлением нового объекта в систему. Первый аргумент — куда добавляется новый объект (напр. если добавляется задача, то в первом аргументе колонка, куда добавляется), второй аргумент — имя нового объекта. Если функция возвращает false, добвление отменяется.
Items.onBeforeAdd = function (a, s) {
if (a.type === 'Column') { // only when a task is added to a column
// Prevent task addition with name “test”
if (s === 'test') {
return false;
}
}
// We return true because we want to proceed with the addition
// If it is needed to prevent addition, then return false
return true;
};
Items.onRename (item, newName)
Эта функиця вызывается, когда какой-нибудь объект переименовывается (задача, колонка, доска или проект).
Items.onRename = function (obj, n) {
if (obj.type === 'Task') { // only for task renaming
obj.name = n + '!!!';
// Return false to stop the default action — renaming the
// object to a name specified by the user (n variable in our case)
return false;
}
// Make the default action in all other cases
return true;
};
Items.onDelete (item)
Эта функция вызывается, когда юзер удаляет какой-нибудь объект (задачу, колонку, доску или проект).
Items.onDelete = function (object) {
// Preventing deletion of objects whose names start with 'Important'
if (object.name.slice(0, 9) === 'Important') {
Notify.warn('You’re not allowed to delete important things!');
return false;
}
// Otherwise proceed with deletion
return true;
};
Items.onClick (item)
Эта функция вызывается, когла пользователь щелкает на задачу, заголовок колонки, вкладку доски или на проект.
Items.onClick = function (object) {
Notifier.success('You’ve clicked ' + object.name);
return true;
};
Items.onList (item, items)
Эта функция вызывается, когда система показывает список объектов внутри какого-то объекта. Например, когда отрисовывается список задач внутри колонки или список колонок внутри доски и так далее.
Items.onList = function (obj, arr) {
// obj - is a container which child objects are going to be listed
// arr - is its actual children objects
if (obj.type === 'Column') {
// In case of task list in a column
// Here obj is a column and arr is all its tasks
// We return only 5 elements from the array of tasks,
// this result is going to be shown to the user
// So the user is going to see no more than 5 tasks in every column
return arr.slice(0, 5);
}
// Otherwise, we return the default
return arr;
};
Items.onMove (item, from, to)
Эта функция вызывается, когда пользователь перемещает объект из одного места в другое (например, перемещает задачу из одной колонки в другую).
Items.onMove = function (obj, from, to) {
// Getting some board by id
var brd = Items.get('... board id here');
// Suppose we want to disable moving tasks in this board
// So we check if the parent object of 'from' is this board
// If so, then 'from' object is some column inside this board
// and that’s why 'obj' is some task inside this column which the user is
// trying to move somewhere. So we return false to prevent it.
if (from.up().id === brd.id) {
Notifier.warn('Moving tasks inside ' + brd.name + ' board is disallowed');
return false;
}
return true;
};
Items.onUpdate (item)
Эта функция вызывается когда какой-то объект системы был обновлён (проект, доска, колонка, задача). Учитываются не только обновления текущим пользователем, но и другие обновления, приходящие с сервера. Если произошло обновление чата, то вызывается эта функция с соответсвующей задачей в качестве аргумента.
var column = Items.get('...');
// Let’s store the number of tasks
// in this column in some variable
var tasksCount = column.list().length;
// If we want to recalculate tasksCount
// on changes, then we can do it as follows
Items.onUpdate = function (obj) {
if (obj.id === column.id) {
tasksCount = column.list().length;
}
};
Items.isHidden (item)
Items.isHidden = function (x) {
// We do not want to show some column to user Mike
if (Current.user.email === 'mike@example.com') {
// Better to compare id
if (x.id === '... here goes column id') {
return true;
}
}
return false;
};
Items.allowDelete (item)
Если эта функция возвращает false для какого-нибудь объекта в какой-нибудь ситуации, то кнопка удаления не показывается для этого объекта (задачи, колонки, доски или проекта).
Items.allowDelete = function (object) {
if (object.type === 'Column') {
// Hide all delete buttons from all columns
return false;
}
return true;
};
Items.allowAdd (item)
Если эта функция возвращает false для какого-нибудь объекта, тогда кнопка добавления не отображается внутри этого объекта.
Items.allowAdd = function (a) {
// Remove “Add a new task” button from all columns
if (a.type === 'Column') {
return false;
}
return true;
};
Items.allowRename (item)
Если эта функция возвращает false для некоторого объекта, тогда он не может быть переименован через интерфейс.
Items.allowRename = function (object) {
// Disallow renaming to a specific user
if (Current.user.id === '... user id here') {
return false;
}
return true;
};
Items.allowDrag (item)
Если эта функция возвращает false для объекта, то его нельзя перетаскивать.
Items.allowDrag = function (object) {
// Disallow dragging for a specific user
if (Current.user.id === '... user id here') {
return false;
}
return true;
};
Примеры скриптов от разработчиков
Работа Javascript в чате
/**
* Выполняет javascript в чате,
* если сообщение начинается с >>>,
* то оно выполняется как javascript.
* Пример:
* >>> 12 * 12;
* » 144
*/
Chat.onPostMessage = function (msg) {
if (msg.text.slice(0, 3) === '>>>') {
var res = eval(msg.text.slice(3));
msg.text += 'n » ' + res;
msg.setData({js: true});
highlight(msg.getTask());
}
};
// подсвечивает сообщения в чате с выполненным js
function highlight(chat) {
Chat.listMessages(chat).forEach(function (msg) {
if (msg.getData().js) {
msg.ui.style = {
fontFamily: 'Menlo, Monaco, Courier New, monospace',
background: '#fffded'
};
}
});
}
// подсвечиваем сообщения с js при старте скрипта
// и при открытии нового чата
if (Current.chat) {
highlight(Current.chat);
}
Current.onChatChange = function (oldChat, newChat) {
if (newChat) {
highlight(newChat);
}
};
Добавляет кнопку «время ответа» в панель стикеров досок.
/**
* Добавляет кнопку “время ответа” в панель стикеров досок.
* Если кнопка нажата, то на всех задачах доски отображается
* время ответа на сообщения в чате.
*
* Чтобы время считалось, нужно, чтобы на задачу был назначен
* ответственный (стикер пользователь). Тогда время ответа —
* это среднее время, за которое отвественный пользователь
* отвечает другим пользователям в чате.
*
* Если назначенный на задачу пользователь не ответил на
* последнее сообщение, задача посвечивается красным.
*/
// добавим кнопку “время ответа” в панель стикеров
var trigger = UI.text('время ответа');
UI.components.stickerPanel.add(trigger);
trigger.style = {
fontSize: '12px',
lineHeight: '16px',
padding: '0px 10px 1px',
display: 'inline-block',
borderRadius: '10px',
cursor: 'pointer',
border: '1px solid #fff',
verticalAlign: 'top',
marginTop: '3px',
marginRight: '6px',
color: '#fff',
background: 'rgba(100, 100, 100, 0.4)'
};
// эта кнопка включает/выключает отображение времени ответа в доске
trigger.onClick = function () {
var data = Current.board.getData();
data.responseTimes = !data.responseTimes;
Current.board.setData(data);
redraw();
};
redraw(); // redraw отрисовывает время ответов в задачах доски см. ниже
Current.onBoardChange = redraw;
// при обновлении задач из доски, пересчитаем время ответов
Items.onUpdate = function (item) {
if (!Current.board || !Current.board.getData().responseTimes) {
return;
}
if (item.type === 'Task' && item.up().up().id === Current.board.id) {
updateTask(item);
redrawTask(item);
}
};
// отрисовывает время ответов в задачах доски
function redraw() {
if (!Current.board) {
return;
}
var enabled = Current.board.getData().responseTimes;
Current.board.list().forEach(function (col) {
col.list().forEach(function (task) {
if (enabled) {
updateTask(task);
redrawTask(task);
} else {
task.ui.clear();
}
});
});
var enabled = Current.board.getData().responseTimes;
if (enabled) {
trigger.style.background = 'rgba(70, 200, 50, 0.6)';
trigger.refresh();
} else {
trigger.style.background = 'rgba(100, 100, 100, 0.4)';
trigger.refresh();
}
}
// обновляет время ответа в задаче
function updateTask(task) {
var assigned = Users.getAssigned(task);
if (!assigned) {
return;
}
var data = task.getData();
if (data.lastMessageTime &&
data.lastMessageTime >= lastMessageTime) {
return;
}
var msg = Chat.listMessages(task);
if (msg.length === 0) {
return;
}
var count = 0;
var totalTime = 0;
var lastQuestionTime = 0;
var lastMessageTime = 0;
var asked = false;
for (var i = 0; i < msg.length; i++) {
var m = msg[i];
if (m.from.name === '_') { // YouGile system message
continue;
}
lastMessageTime = m.timestamp;
if (m.from.id !== assigned.id) {
if (!asked) {
lastQuestionTime = m.timestamp;
asked = true;
}
} else {
if (asked) {
totalTime += m.timestamp - lastQuestionTime;
asked = false;
count++;
}
}
}
if (count > 0 || asked) {
var result = Math.round(totalTime / count / 1000);
data.responseTime = result;
data.noResponse = asked;
data.lastMessageTime = lastMessageTime;
task.setData(data);
}
}
// переписовывает всемя ответа в задаче
function redrawTask(task) {
task.ui.clear();
var assigned = Users.getAssigned(task);
if (!assigned) {
var warn = UI.text('(пользователь не назачен)');
warn.style = {
fontSize: '13px',
color: '#999'
};
task.ui.add(warn);
return;
}
var time = task.getData().responseTime;
var noResponse = task.getData().noResponse;
if (time || noResponse) {
var txt = UI.text(formatTime(time));
txt.style = {
fontSize: '13px',
color: time < 3600 ? '#999' : '#e77'
};
if (noResponse) {
task.ui.style.background = '#ffede5';
task.ui.refresh();
}
if (time) {
task.ui.add(txt);
}
}
}
// конвертирует количество секунд в
// удобный для чтения формат, напр.: 200 => 3m 20s
function formatTime(sec) {
var s = sec % 60;
var m = (sec - s) / 60;
var h = (m - m % 60) / 60;
m = m % 60;
var d = (h - h % 24) / 24;
h = h % 24;
var parts = [];
parts.push(s + 's');
if (m) {
parts.push(m + 'm');
if (h) {
parts.push(h + 'h');
if (d) {
parts.push(d + 'd');
}
}
}
parts.reverse();
return parts.join(' ');
}
Настраивать свой фон для каждой доски отдельно.
/**
* Позволяет настраивать свой фон для каждой доски отдельно.
* Интерфейс изменения фона виден администраторам.
* Интерфейс открывается по нажатию Ctrl+1
*/
if (Current.user.isAdmin) {
Notifier.success('Нажмите Ctrl+1 чтобы открыть настройку фона');
}
// интерфейс настройки фона
var dialog = UI.panel(); // панель выбора фона
dialog.style.display = 'none'; // сначала не видна, нужно нажать Ctrl+1
var allBg = UI.panel(); // здесь будут уже загруженные фоны
var addBgBtn = UI.button('Загрузить новый фон');
var cancelBtn = UI.button('Отмена');
var clearBtn = UI.button('Очистить фон');
// добавляем все кнопки в панель
dialog.add(addBgBtn);
dialog.add(cancelBtn);
dialog.add(clearBtn);
dialog.add(allBg);
// события при нажатии на кнопки
cancelBtn.onClick = closeDialog;
clearBtn.onClick = function () {
setBg('');
};
addBgBtn.onClick = function () {
closeDialog();
App.chooseAndUpload(function (url) {
var data = Current.company.getData();
data.backgrounds = data.backgrounds || [];
data.backgrounds.push(url);
Current.company.setData(data);
setBg(url);
});
};
// по нажатию Ctrl+1 показываем панель настройки
Events.onKeyDown = function (e) {
if (e.keyCode === 49 && Events.isCtrlPressed()) { // Ctrl+1
// чтобы Ctrl+1 не перехватывалось системными комбинациями клавиш
e.preventDefault();
if (!Current.board) {
Notifier.warn('Зайдите в доску, чтобы установить её фон ' +
'и тогда нажмите Ctrl+1 ещё раз!');
return;
}
if (dialog.style.display === 'none') {
openDialog();
} else {
closeDialog();
}
}
};
// открытие интерфейса настройки
// и перерисовка списка загруженных фонов
// (возможно, кто-то добавил фон)
function openDialog() {
dialog.style.display = 'block';
dialog.refresh();
allBg.clear();
// добавляем загруженные фоны
var all = Current.company.getData().backgrounds || [];
all.forEach(function (url) {
var prev = UI.image(App.getPreviewUrl(url));
prev.style = imgStyle();
allBg.add(prev);
prev.onClick = function () {
setBg(url);
closeDialog();
};
});
}
// скрывает интерфейс настройки
function closeDialog() {
dialog.style.display = 'none';
dialog.refresh();
}
// устанавливает фон с заданным url в текущую доску
function setBg(url) {
var boardData = Current.board.getData();
boardData.bg = url;
Current.board.setData(boardData);
refreshBg();
}
// прикрепляет интерфейс настройки к текущей
// доске, но он при этом не виден, чтобы
// он показался используется openDialog();
function addBgChooser() {
if (!Current.user.isAdmin || !Current.board) {
return;
}
Current.board.ui.add(dialog);
}
// отрисовывает нужный фон,
// соответствуюший текущей доске
function refreshBg() {
if (!Current.board) {
return;
}
var bg = Current.board.getData().bg || '';
UI.background = bg;
}
refreshBg();
addBgChooser();
Current.onBoardChange = function () {
closeDialog();
addBgChooser();
refreshBg();
};
// styles
//
function imgStyle () {
return {
display: 'inline-block',
margin: '10px',
cursor: 'pointer',
height: '100px',
overflow: 'hidden',
boxShadow: '0 1px 12px -2px #fff',
borderRadius: '2px'
};
};
function btnStyle() {
return {
background: 'none',
color: '#fff',
background: '#344',
border: '1px solid #bbb',
margin: '2px 4px 0 0'
};
}
addBgBtn.style = btnStyle();
cancelBtn.style = btnStyle();
clearBtn.style = btnStyle();
Другие скрипты
// Скрипт: Убирает адресата из названия задачи в определенной колонке (для интеграции с почтой)
const column1 = "7c270d09-33a3-473c-8963-0b1c54a10f25"; // тут пишем нужную колонку
// Ниже можно написать и другие колонки по образцу выше
const columns = [column1]; // указываем эти колонки через запятую
function cutFirst(text) {
return text.substr(text.indexOf(" ") + 1);
}
Items.onUpdate = function (obj) {
// При обновлении
if (columns.includes(obj.id)) {
// Если колонка совпадает
for (const task of obj.list()) {
// Перебираем ее задачи
if (task.name.includes("@")) {
// Если есть @
task.name = cutFirst(task.name); // Обрезаем название по первому пробелу
}
}
}
};
// Скрипт: Множит по клику на карточку задачи через Shift задачи на исполнителей
Items.onClick = function (object) { // при клике
if (object.type === 'Task') { // на задачу
if (Events.isShiftPressed()) { // с зажатым Shift
const originalName = object.name; // берем название
const executors = Users.getAssigned(object, true); // исполнителей
const checklists = Chat.getSubtasks(object); // чеклисты
const description = Chat.getDescription(object); // описание
const usersInChat = Chat.listUsers(object);
function stickersInTask() { // функция для сбора стикеров в задаче
const stickers = Stickers.getAll().filter(sticker => Stickers.isPinned(object, sticker));
const values = stickers.map(sticker => ({
'key': sticker,
'value': Stickers.getValue(object, sticker)
}));
return values;
}
if (executors.length > 1) { // если несколько исполнителей
Users.assignUser(executors[0], object); // оставляем в задаче первого исполнителя
object.name = `[${executors[0].name}] ${originalName}`; // ставим его имя в название
usersInChat.forEach(user => Chat.removeUser(object, user)); // чистим чат
Chat.addUser(object, executors[0]); // подписываем
executors.shift(); // удаляем из массива
executors.forEach(executor => { // у остальных
object.up().add(`[${executor.name}] ${originalName}`); // добавляем по задаче
const currentTask = object.up().find(`[${executor.name}] ${originalName}`); // ищем ее
stickersInTask().forEach(sticker => { // для каждого стикера
setTimeout(function () { // таймаут для стабильности
Stickers.pin(currentTask, sticker.key, sticker.value); // ставим стикеры
Chat.setSubtasks(currentTask, checklists); // чеклисты
Chat.setDescription(currentTask, description); // описание
Users.assignUser(executor, currentTask); // исполнителя
usersInChat.forEach(user => Chat.removeUser(currentTask, user)); // чистим чат
Chat.addUser(currentTask, executor); // подписываем
}, 1000);
});
});
}
return false; // не открываем задачу
}
}
return true; // иначе открываем
};
// Скрипт: Добавляет к названию задачи названия проекта и доски
Items.onAdd = function (object) { // при добавлении
if (object.type === 'Task' && object.up()) { // если задача на доске
object.name = `[${object.up().up().up().name}, ${object.up().up().name}] ${object.name}`; // переименовываем
}
};
// Скрипт: Берет первые три цифры из описания и крепит их к задаче числовым стикером
const yourNumSticker = Stickers.get('Число'); // напишите название своего стикера
Items.onUpdate = function(obj) { // при обновлении
if (obj.type === 'Task') { // если задача
let descDump = obj.getData().description; // берем наш дамп описания задачи
if (descDump === undefined) {
descDump = '';
} // если дампа нет, то оставляем его пустым
const currenDesc = Chat.getDescription(obj); // берем описание задачи
if (descDump !== currenDesc) { // если разное с дампом
const twoLetters = parseInt(currenDesc.slice(0, 6).replaceAll(/</?[^>]+(>|$)/gi, "")); // берем первые знаки как числа
const numSticker = yourNumSticker; // берем стикер
if (!Number.isNaN(twoLetters)) { // если нормальное число
Stickers.pin(obj, numSticker, twoLetters); // крепим стикер
}
}
obj.setData({
description: currenDesc
}); // сохраняем текущее описание в дамп на будущее
}
};
// Скрипт: При клике на карточку задачи через Shift назначать себя исполнителем, а через Alt — еще удалить участников чата
Items.onClick = function (object) { // при клике
if (object.type === 'Task') { // на задачу
if (Events.isShiftPressed()) { // если зажата Shift
Users.assignUser(Current.user, object); // назначаем себя на задачу
Chat.addUser(object, Current.user); // добавляем себя в чат
return false; // не открываем задачу
}
if (Events.isAltPressed()) { // если зажата Alt
const listInChat = Chat.listUsers(object); // собираем участников чатов
listInChat.forEach(user => Chat.removeUser(object, user)); // удаляем их из чата
Users.assignUser(Current.user, object); // назначаем себя
Chat.addUser(object, Current.user); // подписываем себя на чат
return false; // не открываем задачу
}
}
return true; // открываем задачу
};
// Скрипт: Прочитывает все чаты по Ctrl + Shift + Backspace
Events.onKeyUp = function (event) { // следим за клавиатурой
if (Events.isShiftPressed() &&
Events.isCtrlPressed() &&
event.keyCode === 8) { // и Backspace
const warn = confirm("Вы действительно хотите прочитать все чаты?"); // размещаем предупреждение
if (warn) { // если все ок
for (const project of Current.company.list()) // разбиваем компанию на проекты
for (const boards of project.list()) // проекты на доски
for (const columns of boards.list()) // доски на колонки
for (const task of columns.list()) { // колонки на задачи
if (Chat.isUserInChat(task, Current.user)) { // берем только подписки
Chat.markRead(task, Current.user); // прочитываем задачи
}
}
}
}
};
// Скрипт: Открывает созданную задачу, если не зажата Alt. Если зажата — добавляет как обычно
Items.onAdd = function (task) { // при добавлении
if (task.type === 'Task') { // задачи
!Events.isAltPressed() ? Chat.open(task) : true; // при незажатой Alt открывает чат, иначе просто добавляет задачу
}
};
// Скрипт: Создает несколько задач из списка
Items.onBeforeAdd = function (location, name) { // перед добавлением
if (name.includes('n')) { // если есть перенос строк
const tasks = name.split('n').reverse(); // режем строки на массивы по переносу обратным порядком
tasks.forEach(name => location.add(name)); // добавляем задачи
return false; // не добавляя изначальную
}
};
// Скрипт: Добавляет второго исполнителя ко всем задачам первого исполнителя
// Для запуска скрипта нужно из-под администратора нажать Ctrl + Shift + / , в первом поле заполнить почту уже существующего исполнителя, во втором — нового исполнителя.
// Скрипт включает второго пользователя во все проекты существующего под ролью Сотрудник, не забудьте ее сменить.
// Работает только в браузере с возможностью открыть запрос (prompt).
Events.onKeyUp = function (event) { // следим за клавиатурой
if (Events.isShiftPressed() &&
Events.isCtrlPressed() &&
Current.user.isAdmin &&
event.keyCode === 191) { // если вызывает админ через Shift, Ctrl и /
const assignedUserEmail = prompt('Введите email текущего исполнителя'); // запрашиваем первый email
const userToAssignEmail = prompt('Введите email нового исполнителя'); // запрашиваем второй
const assignedUser = Users.listAll().find(user => user.email === assignedUserEmail); // найдем текущего исполнителя
const userToAssign = Users.listAll().find(user => user.email === userToAssignEmail); // найдем второго исполнителя
// если email не найдены, то показываем ошибку
if (assignedUser === undefined) { alert('Email текущего исполнителя не найден'); }
if (userToAssign === undefined) { alert('Email нового исполнителя не найден'); }
if (assignedUser !== undefined && userToAssign !== undefined) { // если все нормально
for (const project of Current.company.list())
for (const boards of project.list()) // проекты на доски
for (const columns of boards.list()) // доски на колонки
for (const task of columns.list()) { // колонки на задачи
if (Users.isUserAssigned(assignedUser, task)) { // если старый сотрудник подписан
const usersInTask = Users.getAssigned(task, true); // получим список уже подписанных сотрудников
const newUsersList = [...usersInTask, userToAssign]; // добавим в него нового
Users.assignUser(newUsersList, task); // применим этот список
Users.addToChat(userToAssign, task); // подпишем в чат
if (Users.isInProject(assignedUser, project)) { // в проекты, в которые добавлен текущий исполнитель
Users.addToProject(userToAssign, project); // подпишем второго исполнителя
}
}
}
}
}
};
// Скрипт: Ограничивает количество задач в колонке
// В скрипте нужно заполнить два поля: id колонки в column и ограничение количества задач в ней
const column = '0da6cadd-7717-41cd-8dbe-6b571d15301a'; // id колонки
const count = 10; // максимальное количество задач в колонке
Items.onBeforeAdd = function (location, name) { // смотрим перед добавлением
if (location.id === column) { // если колонка совпадает с нужной
const columnCount = location.list().filter(x => x.isArchived() === false).length; // получаем количество неархивных задач
if (columnCount >= count) { // если больше нашего количества
alert('В колонке больше 10 задач!'); // выводим сообщение
return false; // запрещаем добавление
}
} else {
return true; // разрешаем
}
};
Items.onMove = function (object, from, to) { // при перемещении задачи
if (to.id === column) { // если колонка назначения совпадает с нужной
const columnCount = to.list().filter(x => x.isArchived() === false).length; // получаем количество неархивных задач
if (columnCount >= count) { // если больше нашего количества
alert('В колонке больше 10 задач!'); // выводим сообщение
return false; // запрещаем добавление
}
} else {
return true; // разрешаем
}
};
// Скрипт: Отписывает от всех задач доски через кнопку
// Работает только в браузере с возможностью открыть диалог подтверждения (confirm).
Current.onBoardChange = function (oldBoard, newBoard) {
// При смене доски
const boardList = Current.project.list(); // собираем все проекты
boardList.forEach(function (board) {
// Для каждой доски
const btn = UI.button("Отписаться от всех задач"); // создаем кнопку
board.ui.clear(); // предварительно очищаем, чтобы удалить предыдущий и не дублировать
btn.onClick = function (e) {
// В кнопке
const warn = confirm("Вы действительно хотите отписаться от всех задач доски?"); // размещаем предупреждение
if (warn) {
// Если все ок
for (const column of Current.board.list()) // для колонок в доске
for (const task of column.list()) {
// для задач в колонке
Chat.removeUser(task, Current.user); // отписываем пользователя
}
}
};
board.ui.add(btn); // добавляем кнопку в интерфейс
});
};
// Скрипт: Подписка на все чаты задач (при их создании)
// Скрипт работает только для администраторов компании, внутри скрипта нужно указать их список
// Не работает для подзадач.
// Нужно заполнить e-mail-ы администраторов в listOfEmails, которые будут подписываться на чаты
const listOfEmails = ["example1@example.com", "example2@example.com"]; // список email-ов для подписки
const findId = listOfEmails.map((email) => Users.get(email)); // переведем почты в id
const justAdmins = findId.filter((user) => user.isAdmin); // отфильтруем только администраторов
Items.onAdd = function (task) { // при добавлении
if (task.type === "Task") { // задачи
justAdmins.forEach((admin) => Chat.addUser(task, admin)); // подписываем на чат
}
};
// Скрипт: Уведомление в чат о перемещении задачи
// Не работает для подзадач.
Items.onMove = function (task, from, to) { // при перемещении
if (task.type === 'Task') { // задачи
Chat.postMessage(task, '', `Сотрудник ${Current.user.name} переместил задачу из колонки «${from.name}» в колонку «${to.name}».`); // пишем сообщение в чат
}
};
// Скрипт: Архивация задач при выполнении
// Может быть реализовано штатным функционалом YouGile: перейдите в Настройки доски > Основные и убедитесь, что галочка "Автоматически архивировать выполненные задачи" включена.
Items.onUpdate = function(obj) { // вызываем при обновлении
if (obj.isCompleted() === true) { // если задача выполнена
obj.setTaskCompleted(true); // то архивируем ее
}
};
// Скрипт: Считает количество задач в колонке и печатает его в интерфейсе
// Пишет количество задач в колонках при отрисовке страницы и обновлении объектов.
// Вносить в него ничего не надо, можно в p.style поменять что-то на свой вкус.
Current.onBoardChange = function (oldBoard, newBoard) { // работаем при смене доски
const listOfColumns = Current.board.list(); // получаем список колонок
listOfColumns.forEach(function (i) { // перебираем каждую колонку
if (!Items.get(i.id).isMirror() && !Items.get(i.id).isReport()) { // если колонка не зеркало и не сводка
const column = Items.get(i.id); // получаем id колонки
const numberTasks = column.list(); // берем общее количество задач в колонке
const tasksIsNotArchived = numberTasks.filter(x => x.isArchived() === false); // забираем задачи не в архиве
const p = UI.panel(); // создаем контейнер
column.ui.clear(); // предварительно очищаем, чтобы удалить предыдущий и не дублировать
column.ui.add(p);
p.style = {
margin: 'auto',
width: '100px',
padding: 'auto',
color: '#572'
}; // оформляем контейнер
p.add(UI.text('Задач: ' + tasksIsNotArchived.length)); // пишем текст
}
});
};
Items.onUpdate = function (obj) { // вызываем функцию при обновлении, чтобы работало в реальном времени
Current.onBoardChange();
};
// Скрипт: Запрашивает обязательный комментарий при перемещении задачи в другую колонку и печатает его в описание
// Работает только в браузере с возможностью открыть запрос (prompt).
Items.onMove = function(obj, from, to) {
if (to.id === 'id колонки') { // если id колонки совпадает
let comment = prompt('Введите комментарий'); // запрашиваем комментарий
if (!comment) { // если комментария нет
return false; // отказываем
} else {
Chat.postMessage(obj, '', `Задача перемещена пользователем ${Current.user.name}`); // пишем в чат, что переместили задачу
Chat.setDescription(obj, Chat.getDescription(obj) + '<br> <p><b>Комментарий пользователя ' + Current.user.name + ': </b></p>' +
'<p>' + comment + '</p>'); // пишем в описание комментарий
}
return true;
}
};
// Скрипт: Назначение пользователя в определенной колонке
// Добавляет исполнителя в задачу, если ее переместить в колонку и у задачи нет исполнителя.
// Надо вписать id колонки и id пользователя.
// Для текущего пользователя (кто перемещает задачу) можно вместо id пользователя вписать Current.user.id без кавычек, это сделает его исполнителем.
Items.onMove = function(obj, from, to) {
const task = Items.get(obj.id); // берем id таска
const user = Users.get('id пользователя'); // берем id пользователя
if (to.id === 'id колонки' && !Users.getAssigned(task)) { // если id колонки совпадает и в задаче нет исполнителя
Users.assignUser(user, task); // назначаем пользователя
}
};
// Скрипт: Скрывает задачу не из "белого списка" аккаунтов, если на нее навесить определенный стикер
// userRestrict — массив, в который вносятся email-ы, для которых задача скрывается
Items.isHidden = function(object) { // функция скрытия
const stickerRestrict = Stickers.get('Ограничение'); // забираем нужный текстовый стикер
const allowToSee = Stickers.getValue(object, stickerRestrict); // забираем состояние этого стикера
const userRestrict = ['example1@example.com', 'example2@example.com']; // массив почт, для которых задача скроется
function checkEmail(email) { // функция, которая возвращает совпадение запрещенного списка с текущим пользователем
return email === Current.user.email;
}
const restrict = userRestrict.find(checkEmail); // ищет совпадение
if (object.type === 'Task' && allowToSee === 'Запрет' && restrict !== undefined) { // если задача со стикером "Запрет" и текущий пользователь попадает в список
return true; // скрываем
}
return false; // в остальных случаях должно быть открыто
};
// Скрипт: Сообщение в чат при удалении исполнителя
Stickers.onUnpin = function(task, sticker) { // при откреплении стикера
if (sticker.name === 'User') { // если стикер — исполнитель
const userID = Stickers.getValue(task, sticker); // берем id исполнителя
const userName = Users.get(userID).name; // получаем его имя
Chat.postMessage(task, '', `${userName} удален из исполнителей задачи`); // пишем в чат задачи
}
return true;
};
// Скрипт: Сообщение в чат при добавлении файла в описание задачи
// Ниже — единожды запускаемый процесс просмотра всех задач для фиксации дампа описания
for (const project of Current.company.list())
for (const boards of project.list())
for (const columns of boards.list())
for (const task of columns.list()) {
const dump = task.getData(); // читаем дамп
const desc = Chat.getDescription(task); // и описание
if (dump.description !== desc) { // если не совпадают
dump.description = desc;
task.setData(dump); // пишем дамп
}
}
// Дальше работаем по обновлению задач
Items.onUpdate = function(obj) { // при обновлении
if (obj.id === Current.chat.id) { // фильтруем по открытой задаче (избегаем множества срабатываний)
const dump = obj.getData(); // читаем дамп
const currenDesc = Chat.getDescription(obj); // берем описание задачи
const diff = findDiff(dump.description, currenDesc); // сравниваем и находим разницу
if (diff.includes('rel="noopener noreferrer"')) { // если в разнице есть свойство вложения
Chat.postMessage(obj, '', 'Файл добавлен в описание'); // пишем в чат
}
dump.description = currenDesc; // переписываем дамп в любом случае
obj.setData(dump); // сохраняем текущее описание в дамп на будущее
}
};
function findDiff(str1, str2) { // это функция, которая сравнивает две строки и выводит между ними разницу
let diff = "";
str2.split('').forEach(function(val, i) {
if (val != str1.charAt(i))
diff += val;
});
return diff;
}
// Скрипт: Сообщение в чат о прикреплении/изменении дедлайна
// Реагирует на создание и изменение дедлайна задачи, отписывая сообщение в чат задачи.
// Удаление дедлайна игнорирует
Stickers.onPin = function(task, sticker, value) { // при прикреплении стикера
if (sticker.type === 'Deadline' && value !== undefined) { // если это дедлайн и у него есть значение
Chat.postMessage(task, '', 'Прикреплен/изменен дедлайн'); // пишем сообщение в чат
}
};
// Скрипт: Не дает добавить задачу, если есть с таким же названием в компании
// Не работает для подзадач.
Items.onBeforeAdd = function (location, name) { // перед добавлением
if (location.type === 'Column') { // задачи
for (const project of Current.company.list())
for (const boards of project.list())
for (const columns of boards.list())
for (const task of columns.list()) {
if (name == task.name) { // если название совпадает
Notifier.error('Задача с таким названием уже есть'); // показываем ошибку
return false; // запрещаем
}
}
return true; // разрешаем, если ничего не запретилось
}
};
// Скрипт: Отправка сообщения в чат при изменении чеклистов
Items.onUpdate = function(obj) { //при обновлении
if (obj.id === Current.chat.id) { //если объект — текущий чат
const checkListsFact = Chat.getSubtasks(obj); // получаем чеклисты
const checkListsDump = obj.getData().checklists; // получаем дамп записи чеклистов
if (JSON.stringify(checkListsFact) !== JSON.stringify(checkListsDump)) { //если дамп и чеклисты не совпадают
if (checkListsDump === undefined) { //если дамп пуст
obj.setData({
checklists: checkListsFact
}); //записываем дамп
} else if (checkListsFact.length !== checkListsDump.length) { //иначе сравниваем длину чеклста и дампа.
// если разница есть → что-то делали со списком чеклистов
obj.setData({
checklists: checkListsFact
}); // записываем дамп
Chat.postMessage(obj, Users.get(''), 'Изменен список чеклиста') // пишем в чат об изменении списка чеклиста
} else { //иначе → если менялся не список чеклиста, то менялся пункт чеклиста
obj.setData({
checklists: checkListsFact
}); // записываем дамп
Chat.postMessage(obj, Users.get(''), 'Изменен пункт чеклиста') // пишем в чат, что менялся пункт чеклиста
}
}
}
};
// Скрипт: Копирует id задачи в буфер обмена по Ctrl + Alt + C
Events.onKeyDown = function (event) {
if (
Current.chat &&
Current.chat.type === "Task" && //если задача
Events.isCtrlPressed() && //и зажат контрол
Events.isAltPressed() && //и альт
event.keyCode === 67 // и C
) {
const companyTail = Current.company.id.substring(Current.company.id.length - 12) // режеим хвостик
const taskTail = Current.chat.id.substring(Current.chat.id.length - 12); //режеим хвостик
App.copyToClipboard(
`https://yougile.com/team/${companyTail}/#chat:${taskTail}`,
); // в буфер
Notifier.success("Ссылка на тикет скопирована"); // уведомление
}
}
Мои скрипты
Множество задач из списка
Создать множество задачи из большого сообщения, которое вставляем в название страницы и нажимаем enter. В начале кода стоит удаление символов [ ] и даты. Вы перепишите функцию под свои «лишние» символы, которые надо удалить.
// Функция для удаления символов и дат в начале строки
function cleanTaskText(text) {
// Удаляем символы - [ ] и дату в формате ДД.ММ.ГГГГ или с тире
text = text.replace(/^s*-s*[s*]s*d{2}.d{2}.d{4}s*[-–—]?s*/, '');
// Обрезаем лишние пробелы в начале и конце строки
return text.trim();
}
// Отслеживаем добавление задачи
Items.onBeforeAdd = function (location, name) {
if (name.includes('n')) { // Если есть переносы строк
const tasks = name.split('n').reverse(); // Разделяем текст по строкам и переворачиваем массив
tasks.forEach(taskName => {
const cleanedTask = cleanTaskText(taskName); // Очищаем от символов и дат
if (cleanedTask) {
location.add(cleanedTask); // Добавляем очищенную задачу
}
});
return false; // Не добавляем исходный текст
}
};
Парсер числа из названия и установка стикера
Добавляет стикер суммы, если в начале есть сумма. Если стоит 10к, то конвертируется в 10 000. Стикер должен быть типа и называться «Сумма».
/**
* Скрипт для автоматического добавления стикера "Сумма" к задачам.
* Работает только для указанного проекта.
* Извлекает сумму из названия задачи, независимо от ее позиции.
* Добавляет стикер "Сумма", если он доступен в системе.
* Поддерживает различные форматы записи суммы: рубли, тысячи, со знаком ₽.
*/
// ID целевого проекта
const TARGET_PROJECT_ID = '23c1c7b1-0402-4643-8026-100f887910dc';
// Получаем целевой проект
const targetProject = Items.get(TARGET_PROJECT_ID);
if (!targetProject) {
console.error(`Проект с ID ${TARGET_PROJECT_ID} не найден.`);
return;
}
// Глобальная переменная для хранения стикера "Сумма"
let sumSticker = null;
// Функция для инициализации стикера "Сумма"
function initSumSticker() {
sumSticker = Stickers.get('Сумма');
if (!sumSticker) {
console.error("Стикер 'Сумма' не найден в системе.");
return false;
}
console.log("Стикер 'Сумма' успешно инициализирован.");
return true;
}
// Функция для проверки, принадлежит ли задача целевому проекту
function isTaskInTargetProject(task) {
let currentItem = task;
while (currentItem) {
if (currentItem.id === TARGET_PROJECT_ID) {
return true;
}
currentItem = currentItem.up();
}
return false;
}
// Функция для извлечения суммы из названия задачи
function extractSum(taskName) {
const regex = /(d+(?:s*d+)*(?:,d+)?)s*(руб(?:лей)?|р|₽|k|к)?/i;
const match = taskName.match(regex);
if (match) {
let sum = parseFloat(match[1].replace(/s/g, '').replace(',', '.'));
const unit = match[2] ? match[2].toLowerCase() : '';
if (unit === 'k' || unit === 'к') {
sum *= 1000;
}
return Math.round(sum);
}
return null;
}
// Обработчик события создания задачи
Items.onAdd = function(task) {
if (task && task.type === 'Task' && isTaskInTargetProject(task)) {
console.log(`Создана новая задача: "${task.name}" в целевом проекте`);
if (!sumSticker && !initSumSticker()) {
console.log("Не удалось инициализировать стикер 'Сумма'. Стикер не будет добавлен.");
return;
}
const taskName = task.name;
const sum = extractSum(taskName);
if (sum !== null) {
setTimeout(() => {
try {
Stickers.pin(task, sumSticker, sum.toString());
console.log(`Стикер 'Сумма' со значением ${sum} добавлен к задаче '${taskName}'`);
} catch (error) {
console.error(`Ошибка при добавлении стикера: ${error.message}`);
}
}, 500);
} else {
console.log(`Сумма не найдена в названии задачи '${taskName}'`);
}
}
};
// Функция для добавления стикеров к существующим задачам
function addStickerToExistingTasks() {
if (!sumSticker && !initSumSticker()) {
console.log("Не удалось инициализировать стикер 'Сумма'. Обработка существующих задач прервана.");
return;
}
console.log("Начало обработки существующих задач...");
for (const board of targetProject.list()) {
console.log(`Обработка доски: ${board.name}`);
for (const column of board.list()) {
for (const task of column.list()) {
if (task && task.type === 'Task') {
const sum = extractSum(task.name);
if (sum !== null) {
try {
Stickers.pin(task, sumSticker, sum.toString());
console.log(`Стикер 'Сумма' со значением ${sum} добавлен к существующей задаче '${task.name}'`);
} catch (error) {
console.error(`Ошибка при добавлении стикера к задаче '${task.name}': ${error.message}`);
}
}
}
}
}
}
console.log("Обработка существующих задач завершена.");
}
// Инициализация стикера при загрузке скрипта
if (initSumSticker()) {
console.log(`Скрипт добавления стикера "Сумма" загружен и готов к работе для проекта с ID ${TARGET_PROJECT_ID}`);
} else {
console.error("Скрипт не может работать без стикера 'Сумма'. Пожалуйста, создайте этот стикер в системе.");
}
// Раскомментируйте следующую строку, чтобы добавить стикеры к существующим задачам
// addStickerToExistingTasks();
Добавить стикер месяца к задаче
Добавляет стикер месяца к задаче. У меня стоит форматирование «Сентябрь 24». При создании задачи проверяет, какой месяц и добавляет стикер. По задумке, если будет январь 2025, то он создаст стикер Январь 25. Но надо дожить, чтобы проверить)
let buttonPanel;
function createButton(text, onClick) {
const button = UI.button(text);
button.style = {
margin: '0 10px 0 0',
padding: '10px 15px',
fontSize: '14px',
fontWeight: 'bold',
backgroundColor: '#333',
color: '#fff', // Добавим белый цвет текста для контраста
border: 'none',
borderRadius: '6px',
cursor: 'pointer'
};
button.onClick = onClick;
return button;
}
function countSubscribedTasks(board) {
return board.list().reduce((count, column) =>
count + column.list().filter(task => Chat.isUserInChat(task, Current.user)).length, 0);
}
function countTasksAsExecutor(board) {
const executorSticker = Stickers.get('User');
return board.list().reduce((count, column) =>
count + column.list().filter(task =>
Stickers.getValue(task, executorSticker) === Current.user.id
).length, 0);
}
function unsubscribeFromAllTasks(board) {
let unsubscribedCount = 0;
board.list().forEach(column => {
column.list().forEach(task => {
if (Chat.isUserInChat(task, Current.user)) {
Chat.removeUser(task, Current.user);
unsubscribedCount++;
}
});
});
Notifier.success(`Вы отписались от ${unsubscribedCount} задач на этой доске.`);
}
function removeAsExecutorFromAllTasks(board) {
const executorSticker = Stickers.get('User');
let removedCount = 0;
board.list().forEach(column => {
column.list().forEach(task => {
if (Stickers.getValue(task, executorSticker) === Current.user.id) {
Stickers.unpin(task, executorSticker);
removedCount++;
}
});
});
Notifier.success(`Вы удалены как исполнитель из ${removedCount} задач на этой доске.`);
}
function createOrUpdateButtonPanel(board) {
if (!buttonPanel) {
buttonPanel = UI.panel();
buttonPanel.style = {
display: 'flex',
justifyContent: 'center',
margin: '10px 0'
};
} else {
buttonPanel.clear();
}
const unsubscribeButton = createButton(
`Отписаться от всех задач доски`,
() => {
const count = countSubscribedTasks(board);
if (confirm(`Вы уверены, что хотите отписаться от ${count} задач на этой доске?`)) {
unsubscribeFromAllTasks(board);
}
}
);
const removeExecutorButton = createButton(
`Удалить меня как исполнителя со всех задач`,
() => {
const count = countTasksAsExecutor(board);
if (confirm(`Вы уверены, что хотите удалить себя как исполнителя из ${count} задач на этой доске?`)) {
removeAsExecutorFromAllTasks(board);
}
}
);
buttonPanel.add(unsubscribeButton);
buttonPanel.add(removeExecutorButton);
return buttonPanel;
}
function addButtonPanelToBoard(board) {
const panel = createOrUpdateButtonPanel(board);
board.ui.add(panel);
}
Current.onBoardChange = function(oldBoard, newBoard) {
if (newBoard) {
addButtonPanelToBoard(newBoard);
}
};
if (Current.board) {
addButtonPanelToBoard(Current.board);
}
Добавить кнопки отписки и удаления себя от всех задач на доске
Добавляет две кнопки: отписать и удалить себя со всех задач на доске. Не на проекте, а на доске. При клике показывает окно, в котором указывает, из скольких задач отпишет или удалит.
let buttonPanel;
function createButton(text, onClick) {
const button = UI.button(text);
button.style = {
margin: '0 10px 0 0',
padding: '10px 15px',
fontSize: '14px',
fontWeight: 'bold',
backgroundColor: '#333',
color: '#fff', // Добавим белый цвет текста для контраста
border: 'none',
borderRadius: '6px',
cursor: 'pointer'
};
button.onClick = onClick;
return button;
}
function countSubscribedTasks(board) {
return board.list().reduce((count, column) =>
count + column.list().filter(task => Chat.isUserInChat(task, Current.user)).length, 0);
}
function countTasksAsExecutor(board) {
const executorSticker = Stickers.get('User');
return board.list().reduce((count, column) =>
count + column.list().filter(task =>
Stickers.getValue(task, executorSticker) === Current.user.id
).length, 0);
}
function unsubscribeFromAllTasks(board) {
let unsubscribedCount = 0;
board.list().forEach(column => {
column.list().forEach(task => {
if (Chat.isUserInChat(task, Current.user)) {
Chat.removeUser(task, Current.user);
unsubscribedCount++;
}
});
});
Notifier.success(`Вы отписались от ${unsubscribedCount} задач на этой доске.`);
}
function removeAsExecutorFromAllTasks(board) {
const executorSticker = Stickers.get('User');
let removedCount = 0;
board.list().forEach(column => {
column.list().forEach(task => {
if (Stickers.getValue(task, executorSticker) === Current.user.id) {
Stickers.unpin(task, executorSticker);
removedCount++;
}
});
});
Notifier.success(`Вы удалены как исполнитель из ${removedCount} задач на этой доске.`);
}
function createOrUpdateButtonPanel(board) {
if (!buttonPanel) {
buttonPanel = UI.panel();
buttonPanel.style = {
display: 'flex',
justifyContent: 'center',
margin: '10px 0'
};
} else {
buttonPanel.clear();
}
const unsubscribeButton = createButton(
`Отписаться от всех задач доски`,
() => {
const count = countSubscribedTasks(board);
if (confirm(`Вы уверены, что хотите отписаться от ${count} задач на этой доске?`)) {
unsubscribeFromAllTasks(board);
}
}
);
const removeExecutorButton = createButton(
`Удалить меня как исполнителя со всех задач`,
() => {
const count = countTasksAsExecutor(board);
if (confirm(`Вы уверены, что хотите удалить себя как исполнителя из ${count} задач на этой доске?`)) {
removeAsExecutorFromAllTasks(board);
}
}
);
buttonPanel.add(unsubscribeButton);
buttonPanel.add(removeExecutorButton);
return buttonPanel;
}
function addButtonPanelToBoard(board) {
const panel = createOrUpdateButtonPanel(board);
board.ui.add(panel);
}
Current.onBoardChange = function(oldBoard, newBoard) {
if (newBoard) {
addButtonPanelToBoard(newBoard);
}
};
if (Current.board) {
addButtonPanelToBoard(Current.board);
}
Канал о фрилансе
Там я делюсь фишками о фрилансе: деньги, клиенты, автоматизация, фриланс. Раньше это был закрытый материал с платным доступом. Сейчас он открыт. Иногда подписчикам будет открываться платные материалы. Подписаться