first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -0,0 +1,233 @@
import type { INodeProperties } from 'n8n-workflow';
export const clientOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['client'],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a client',
action: 'Create a client',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a client',
action: 'Delete a client',
},
{
name: 'Get',
value: 'get',
description: 'Get a client',
action: 'Get a client',
},
{
name: 'Get Many',
value: 'getAll',
description: 'Get many clients',
action: 'Get many clients',
},
{
name: 'Update',
value: 'update',
description: 'Update a client',
action: 'Update a client',
},
],
default: 'create',
},
];
export const clientFields: INodeProperties[] = [
/* -------------------------------------------------------------------------- */
/* client:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Client Name',
name: 'name',
type: 'string',
required: true,
default: '',
description: 'Name of client being created',
displayOptions: {
show: {
resource: ['client'],
operation: ['create'],
},
},
},
/* -------------------------------------------------------------------------- */
/* client:delete */
/* -------------------------------------------------------------------------- */
{
displayName: 'Client ID',
name: 'clientId',
type: 'string',
default: '',
displayOptions: {
show: {
resource: ['client'],
operation: ['delete'],
},
},
},
/* -------------------------------------------------------------------------- */
/* client:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Client ID',
name: 'clientId',
type: 'string',
default: '',
displayOptions: {
show: {
resource: ['client'],
operation: ['get'],
},
},
},
/* -------------------------------------------------------------------------- */
/* client:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
operation: ['getAll'],
resource: ['client'],
},
},
default: false,
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
operation: ['getAll'],
resource: ['client'],
returnAll: [false],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 100,
description: 'Max number of results to return',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
resource: ['client'],
operation: ['getAll'],
},
},
default: {},
options: [
{
displayName: 'Archived',
name: 'archived',
type: 'boolean',
default: false,
},
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
description: 'If provided, clients will be filtered by name',
},
{
displayName: 'Sort Order',
name: 'sort-order',
type: 'options',
options: [
{
name: 'Ascending',
value: 'ASCENDING',
},
{
name: 'Descending',
value: 'DESCENDING',
},
],
default: '',
},
],
},
/* -------------------------------------------------------------------------- */
/* client:update */
/* -------------------------------------------------------------------------- */
{
displayName: 'Client ID',
name: 'clientId',
type: 'string',
default: '',
displayOptions: {
show: {
resource: ['client'],
operation: ['update'],
},
},
},
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: ['client'],
operation: ['update'],
},
},
},
{
displayName: 'Update Fields',
name: 'updateFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
operation: ['update'],
resource: ['client'],
},
},
default: {},
options: [
{
displayName: 'Address',
name: 'address',
type: 'string',
default: '',
description: 'Address of client being created/updated',
},
{
displayName: 'Archived',
name: 'archived',
type: 'boolean',
default: false,
},
],
},
];
@@ -0,0 +1,18 @@
{
"node": "n8n-nodes-base.clockify",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Productivity"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/credentials/clockify/"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.clockify/"
}
]
}
}
@@ -0,0 +1,844 @@
import moment from 'moment-timezone';
import { NodeConnectionTypes } from 'n8n-workflow';
import type {
IExecuteFunctions,
IDataObject,
ILoadOptionsFunctions,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import { clientFields, clientOperations } from './ClientDescription';
import { clockifyApiRequest, clockifyApiRequestAllItems } from './GenericFunctions';
import { projectFields, projectOperations } from './ProjectDescription';
import type { IProjectDto } from './ProjectInterfaces';
import { tagFields, tagOperations } from './TagDescription';
import { taskFields, taskOperations } from './TaskDescription';
import { timeEntryFields, timeEntryOperations } from './TimeEntryDescription';
import { userFields, userOperations } from './UserDescription';
import type { IUserDto } from './UserDtos';
import type { IClientDto, IWorkspaceDto } from './WorkpaceInterfaces';
import { workspaceFields, workspaceOperations } from './WorkspaceDescription';
export class Clockify implements INodeType {
description: INodeTypeDescription = {
displayName: 'Clockify',
name: 'clockify',
icon: { light: 'file:clockify.svg', dark: 'file:clockify.dark.svg' },
group: ['transform'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume Clockify REST API',
defaults: {
name: 'Clockify',
},
usableAsTool: true,
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
credentials: [
{
name: 'clockifyApi',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Client',
value: 'client',
},
{
name: 'Project',
value: 'project',
},
{
name: 'Tag',
value: 'tag',
},
{
name: 'Task',
value: 'task',
},
{
name: 'Time Entry',
value: 'timeEntry',
},
{
name: 'User',
value: 'user',
},
{
name: 'Workspace',
value: 'workspace',
},
],
default: 'project',
},
...clientOperations,
...projectOperations,
...tagOperations,
...taskOperations,
...timeEntryOperations,
...userOperations,
...workspaceOperations,
...workspaceFields,
{
displayName: 'Workspace Name or ID',
name: 'workspaceId',
type: 'options',
description:
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
typeOptions: {
loadOptionsMethod: 'listWorkspaces',
},
required: true,
default: [],
displayOptions: {
hide: {
resource: ['workspace'],
},
},
},
...clientFields,
...projectFields,
...tagFields,
...taskFields,
...userFields,
...timeEntryFields,
],
};
methods = {
loadOptions: {
async listWorkspaces(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const rtv: INodePropertyOptions[] = [];
const workspaces: IWorkspaceDto[] = await clockifyApiRequest.call(
this,
'GET',
'workspaces',
);
if (undefined !== workspaces) {
workspaces.forEach((value) => {
rtv.push({
name: value.name,
value: value.id,
});
});
}
return rtv;
},
async loadUsersForWorkspace(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const rtv: INodePropertyOptions[] = [];
const workspaceId = this.getCurrentNodeParameter('workspaceId');
if (undefined !== workspaceId) {
const resource = `workspaces/${workspaceId}/users`;
const users: IUserDto[] = await clockifyApiRequest.call(this, 'GET', resource);
if (undefined !== users) {
users.forEach((value) => {
rtv.push({
name: value.name,
value: value.id,
});
});
}
}
return rtv;
},
async loadClientsForWorkspace(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const rtv: INodePropertyOptions[] = [];
const workspaceId = this.getCurrentNodeParameter('workspaceId');
if (undefined !== workspaceId) {
const resource = `workspaces/${workspaceId}/clients`;
const clients: IClientDto[] = await clockifyApiRequest.call(this, 'GET', resource);
if (undefined !== clients) {
clients.forEach((value) => {
rtv.push({
name: value.name,
value: value.id,
});
});
}
}
return rtv;
},
async loadProjectsForWorkspace(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const rtv: INodePropertyOptions[] = [];
const workspaceId = this.getCurrentNodeParameter('workspaceId');
if (undefined !== workspaceId) {
const resource = `workspaces/${workspaceId}/projects`;
const users: IProjectDto[] = await clockifyApiRequest.call(this, 'GET', resource);
if (undefined !== users) {
users.forEach((value) => {
rtv.push({
name: value.name,
value: value.id,
});
});
}
}
return rtv;
},
async loadTagsForWorkspace(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const rtv: INodePropertyOptions[] = [];
const workspaceId = this.getCurrentNodeParameter('workspaceId');
if (undefined !== workspaceId) {
const resource = `workspaces/${workspaceId}/tags`;
const users: IProjectDto[] = await clockifyApiRequest.call(this, 'GET', resource);
if (undefined !== users) {
users.forEach((value) => {
rtv.push({
name: value.name,
value: value.id,
});
});
}
}
return rtv;
},
async loadCustomFieldsForWorkspace(
this: ILoadOptionsFunctions,
): Promise<INodePropertyOptions[]> {
const rtv: INodePropertyOptions[] = [];
const workspaceId = this.getCurrentNodeParameter('workspaceId');
if (undefined !== workspaceId) {
const resource = `workspaces/${workspaceId}/custom-fields`;
const customFields = await clockifyApiRequest.call(this, 'GET', resource);
for (const customField of customFields) {
rtv.push({
name: customField.name,
value: customField.id,
});
}
}
return rtv;
},
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: INodeExecutionData[] = [];
const length = items.length;
const qs: IDataObject = {};
let responseData;
const resource = this.getNodeParameter('resource', 0);
const operation = this.getNodeParameter('operation', 0);
for (let i = 0; i < length; i++) {
try {
if (resource === 'client') {
if (operation === 'create') {
const workspaceId = this.getNodeParameter('workspaceId', i) as string;
const name = this.getNodeParameter('name', i) as string;
const body: IDataObject = {
name,
};
responseData = await clockifyApiRequest.call(
this,
'POST',
`/workspaces/${workspaceId}/clients`,
body,
qs,
);
}
if (operation === 'delete') {
const workspaceId = this.getNodeParameter('workspaceId', i) as string;
const clientId = this.getNodeParameter('clientId', i) as string;
responseData = await clockifyApiRequest.call(
this,
'DELETE',
`/workspaces/${workspaceId}/clients/${clientId}`,
{},
qs,
);
}
if (operation === 'update') {
const workspaceId = this.getNodeParameter('workspaceId', i) as string;
const clientId = this.getNodeParameter('clientId', i) as string;
const name = this.getNodeParameter('name', i) as string;
const updateFields = this.getNodeParameter('updateFields', i);
const body: IDataObject = {
name,
};
Object.assign(body, updateFields);
responseData = await clockifyApiRequest.call(
this,
'PUT',
`/workspaces/${workspaceId}/clients/${clientId}`,
body,
qs,
);
}
if (operation === 'get') {
const workspaceId = this.getNodeParameter('workspaceId', i) as string;
const clientId = this.getNodeParameter('clientId', i) as string;
responseData = await clockifyApiRequest.call(
this,
'GET',
`/workspaces/${workspaceId}/clients/${clientId}`,
{},
qs,
);
}
if (operation === 'getAll') {
const returnAll = this.getNodeParameter('returnAll', i);
const workspaceId = this.getNodeParameter('workspaceId', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i);
Object.assign(qs, additionalFields);
if (returnAll) {
responseData = await clockifyApiRequestAllItems.call(
this,
'GET',
`/workspaces/${workspaceId}/clients`,
{},
qs,
);
} else {
qs.limit = this.getNodeParameter('limit', i);
responseData = await clockifyApiRequestAllItems.call(
this,
'GET',
`/workspaces/${workspaceId}/clients`,
{},
qs,
);
responseData = responseData.splice(0, qs.limit);
}
}
}
if (resource === 'project') {
if (operation === 'create') {
const workspaceId = this.getNodeParameter('workspaceId', i) as string;
const name = this.getNodeParameter('name', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i);
const body: IDataObject = {
name,
};
Object.assign(body, additionalFields);
if (body.estimateUi) {
body.estimate = (body.estimateUi as IDataObject).estimateValues;
delete body.estimateUi;
}
responseData = await clockifyApiRequest.call(
this,
'POST',
`/workspaces/${workspaceId}/projects`,
body,
qs,
);
}
if (operation === 'delete') {
const workspaceId = this.getNodeParameter('workspaceId', i) as string;
const projectId = this.getNodeParameter('projectId', i) as string;
responseData = await clockifyApiRequest.call(
this,
'DELETE',
`/workspaces/${workspaceId}/projects/${projectId}`,
{},
qs,
);
}
if (operation === 'get') {
const workspaceId = this.getNodeParameter('workspaceId', i) as string;
const projectId = this.getNodeParameter('projectId', i) as string;
responseData = await clockifyApiRequest.call(
this,
'GET',
`/workspaces/${workspaceId}/projects/${projectId}`,
{},
qs,
);
}
if (operation === 'getAll') {
const returnAll = this.getNodeParameter('returnAll', i);
const workspaceId = this.getNodeParameter('workspaceId', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i);
Object.assign(qs, additionalFields);
if (returnAll) {
responseData = await clockifyApiRequestAllItems.call(
this,
'GET',
`/workspaces/${workspaceId}/projects`,
{},
qs,
);
} else {
qs.limit = this.getNodeParameter('limit', i);
responseData = await clockifyApiRequestAllItems.call(
this,
'GET',
`/workspaces/${workspaceId}/projects`,
{},
qs,
);
responseData = responseData.splice(0, qs.limit);
}
}
if (operation === 'update') {
const workspaceId = this.getNodeParameter('workspaceId', i) as string;
const projectId = this.getNodeParameter('projectId', i) as string;
const updateFields = this.getNodeParameter('updateFields', i);
const body: IDataObject = {};
Object.assign(body, updateFields);
if (body.estimateUi) {
body.estimate = (body.estimateUi as IDataObject).estimateValues;
delete body.estimateUi;
}
responseData = await clockifyApiRequest.call(
this,
'PUT',
`/workspaces/${workspaceId}/projects/${projectId}`,
body,
qs,
);
}
}
if (resource === 'tag') {
if (operation === 'create') {
const workspaceId = this.getNodeParameter('workspaceId', i) as string;
const name = this.getNodeParameter('name', i) as string;
const body: IDataObject = {
name,
};
responseData = await clockifyApiRequest.call(
this,
'POST',
`/workspaces/${workspaceId}/tags`,
body,
qs,
);
}
if (operation === 'delete') {
const workspaceId = this.getNodeParameter('workspaceId', i) as string;
const tagId = this.getNodeParameter('tagId', i) as string;
responseData = await clockifyApiRequest.call(
this,
'DELETE',
`/workspaces/${workspaceId}/tags/${tagId}`,
{},
qs,
);
responseData = { success: true };
}
if (operation === 'getAll') {
const returnAll = this.getNodeParameter('returnAll', i);
const workspaceId = this.getNodeParameter('workspaceId', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i);
Object.assign(qs, additionalFields);
if (returnAll) {
responseData = await clockifyApiRequestAllItems.call(
this,
'GET',
`/workspaces/${workspaceId}/tags`,
{},
qs,
);
} else {
qs.limit = this.getNodeParameter('limit', i);
responseData = await clockifyApiRequestAllItems.call(
this,
'GET',
`/workspaces/${workspaceId}/tags`,
{},
qs,
);
responseData = responseData.splice(0, qs.limit);
}
}
if (operation === 'update') {
const workspaceId = this.getNodeParameter('workspaceId', i) as string;
const tagId = this.getNodeParameter('tagId', i) as string;
const updateFields = this.getNodeParameter('updateFields', i);
const body: IDataObject = {};
Object.assign(body, updateFields);
responseData = await clockifyApiRequest.call(
this,
'PUT',
`/workspaces/${workspaceId}/tags/${tagId}`,
body,
qs,
);
}
}
if (resource === 'task') {
if (operation === 'create') {
const workspaceId = this.getNodeParameter('workspaceId', i) as string;
const projectId = this.getNodeParameter('projectId', i) as string;
const name = this.getNodeParameter('name', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i);
const body: IDataObject = {
name,
};
Object.assign(body, additionalFields);
if (body.estimate) {
const [hour, minute] = (body.estimate as string).split(':');
body.estimate = `PT${hour}H${minute}M`;
}
responseData = await clockifyApiRequest.call(
this,
'POST',
`/workspaces/${workspaceId}/projects/${projectId}/tasks`,
body,
qs,
);
}
if (operation === 'delete') {
const workspaceId = this.getNodeParameter('workspaceId', i) as string;
const projectId = this.getNodeParameter('projectId', i) as string;
const taskId = this.getNodeParameter('taskId', i) as string;
responseData = await clockifyApiRequest.call(
this,
'DELETE',
`/workspaces/${workspaceId}/projects/${projectId}/tasks/${taskId}`,
{},
qs,
);
}
if (operation === 'get') {
const workspaceId = this.getNodeParameter('workspaceId', i) as string;
const projectId = this.getNodeParameter('projectId', i) as string;
const taskId = this.getNodeParameter('taskId', i) as string;
responseData = await clockifyApiRequest.call(
this,
'GET',
`/workspaces/${workspaceId}/projects/${projectId}/tasks/${taskId}`,
{},
qs,
);
}
if (operation === 'getAll') {
const returnAll = this.getNodeParameter('returnAll', i);
const workspaceId = this.getNodeParameter('workspaceId', i) as string;
const projectId = this.getNodeParameter('projectId', i) as string;
const filters = this.getNodeParameter('filters', i);
Object.assign(qs, filters);
if (returnAll) {
responseData = await clockifyApiRequestAllItems.call(
this,
'GET',
`/workspaces/${workspaceId}/projects/${projectId}/tasks`,
{},
qs,
);
} else {
qs['page-size'] = this.getNodeParameter('limit', i);
responseData = await clockifyApiRequest.call(
this,
'GET',
`/workspaces/${workspaceId}/projects/${projectId}/tasks`,
{},
qs,
);
}
}
if (operation === 'update') {
const workspaceId = this.getNodeParameter('workspaceId', i) as string;
const projectId = this.getNodeParameter('projectId', i) as string;
const taskId = this.getNodeParameter('taskId', i) as string;
const updateFields = this.getNodeParameter('updateFields', i);
const body: IDataObject = {};
Object.assign(body, updateFields);
if (body.estimate) {
const [hour, minute] = (body.estimate as string).split(':');
body.estimate = `PT${hour}H${minute}M`;
}
responseData = await clockifyApiRequest.call(
this,
'PUT',
`/workspaces/${workspaceId}/projects/${projectId}/tasks/${taskId}`,
body,
qs,
);
}
}
if (resource === 'timeEntry') {
if (operation === 'create') {
const timezone = this.getTimezone();
const workspaceId = this.getNodeParameter('workspaceId', i) as string;
const start = this.getNodeParameter('start', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i);
const body: IDataObject = {
start: moment.tz(start, timezone).utc().format(),
};
Object.assign(body, additionalFields);
if (body.end) {
body.end = moment.tz(body.end, timezone).utc().format();
}
if (body.customFieldsUi) {
const customFields = (body.customFieldsUi as IDataObject)
.customFieldsValues as IDataObject[];
body.customFields = customFields;
}
responseData = await clockifyApiRequest.call(
this,
'POST',
`/workspaces/${workspaceId}/time-entries`,
body,
qs,
);
}
if (operation === 'delete') {
const workspaceId = this.getNodeParameter('workspaceId', i) as string;
const timeEntryId = this.getNodeParameter('timeEntryId', i) as string;
responseData = await clockifyApiRequest.call(
this,
'DELETE',
`/workspaces/${workspaceId}/time-entries/${timeEntryId}`,
{},
qs,
);
responseData = { success: true };
}
if (operation === 'get') {
const workspaceId = this.getNodeParameter('workspaceId', i) as string;
const timeEntryId = this.getNodeParameter('timeEntryId', i) as string;
responseData = await clockifyApiRequest.call(
this,
'GET',
`/workspaces/${workspaceId}/time-entries/${timeEntryId}`,
{},
qs,
);
}
if (operation === 'update') {
const timezone = this.getTimezone();
const workspaceId = this.getNodeParameter('workspaceId', i) as string;
const timeEntryId = this.getNodeParameter('timeEntryId', i) as string;
const updateFields = this.getNodeParameter('updateFields', i);
const body: IDataObject = {};
Object.assign(body, updateFields);
if (body.end) {
body.end = moment.tz(body.end, timezone).utc().format();
}
if (body.start) {
body.start = moment.tz(body.start, timezone).utc().format();
} else {
// even if you do not want to update the start time, it always has to be set
// to make it more simple to the user, if they did not set a start time look for the current start time
// and set it
const {
timeInterval: { start },
} = await clockifyApiRequest.call(
this,
'GET',
`/workspaces/${workspaceId}/time-entries/${timeEntryId}`,
{},
qs,
);
body.start = start;
}
responseData = await clockifyApiRequest.call(
this,
'PUT',
`/workspaces/${workspaceId}/time-entries/${timeEntryId}`,
body,
qs,
);
}
}
if (resource === 'user') {
if (operation === 'getAll') {
const returnAll = this.getNodeParameter('returnAll', i);
const workspaceId = this.getNodeParameter('workspaceId', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i);
Object.assign(qs, additionalFields);
if (returnAll) {
responseData = await clockifyApiRequestAllItems.call(
this,
'GET',
`/workspaces/${workspaceId}/users`,
{},
qs,
);
} else {
qs.limit = this.getNodeParameter('limit', i);
responseData = await clockifyApiRequestAllItems.call(
this,
'GET',
`/workspaces/${workspaceId}/users`,
{},
qs,
);
responseData = responseData.splice(0, qs.limit);
}
}
}
if (resource === 'workspace') {
if (operation === 'getAll') {
const returnAll = this.getNodeParameter('returnAll', i);
responseData = await clockifyApiRequest.call(this, 'GET', '/workspaces', {}, qs);
if (!returnAll) {
qs.limit = this.getNodeParameter('limit', i);
responseData = responseData.splice(0, qs.limit);
}
}
}
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject[]),
{ itemData: { item: i } },
);
returnData.push(...executionData);
} catch (error) {
if (this.continueOnFail()) {
returnData.push({ error: error.message, json: {} });
continue;
}
throw error;
}
}
return [returnData];
}
}
@@ -0,0 +1,18 @@
{
"node": "n8n-nodes-base.clockifyTrigger",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Utility", "Productivity"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/credentials/clockify/"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/trigger-nodes/n8n-nodes-base.clockifytrigger/"
}
]
}
}
@@ -0,0 +1,126 @@
import moment from 'moment-timezone';
import type {
IPollFunctions,
IDataObject,
ILoadOptionsFunctions,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import { NodeConnectionTypes } from 'n8n-workflow';
import type { EntryType } from './EntryType';
import { EntryTypes } from './EntryType';
import { clockifyApiRequest } from './GenericFunctions';
import type { IUserDto } from './UserDtos';
import type { IWorkspaceDto } from './WorkpaceInterfaces';
export class ClockifyTrigger implements INodeType {
description: INodeTypeDescription = {
displayName: 'Clockify Trigger',
icon: { light: 'file:clockify.svg', dark: 'file:clockify.dark.svg' },
name: 'clockifyTrigger',
group: ['trigger'],
version: 1,
description: 'Listens to Clockify events',
defaults: {
name: 'Clockify Trigger',
},
inputs: [],
outputs: [NodeConnectionTypes.Main],
credentials: [
{
name: 'clockifyApi',
required: true,
},
],
polling: true,
properties: [
{
displayName: 'Workspace Name or ID',
name: 'workspaceId',
type: 'options',
description:
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
typeOptions: {
loadOptionsMethod: 'listWorkspaces',
},
required: true,
default: '',
},
// eslint-disable-next-line n8n-nodes-base/node-param-default-missing
{
displayName: 'Trigger',
name: 'watchField',
type: 'options',
options: [
{
name: 'New Time Entry',
value: EntryTypes.NEW_TIME_ENTRY,
},
],
required: true,
default: EntryTypes.NEW_TIME_ENTRY,
},
],
};
methods = {
loadOptions: {
async listWorkspaces(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const rtv: INodePropertyOptions[] = [];
const workspaces: IWorkspaceDto[] = await clockifyApiRequest.call(
this,
'GET',
'workspaces',
);
if (undefined !== workspaces) {
workspaces.forEach((value) => {
rtv.push({
name: value.name,
value: value.id,
});
});
}
return rtv;
},
},
};
async poll(this: IPollFunctions): Promise<INodeExecutionData[][] | null> {
const webhookData = this.getWorkflowStaticData('node');
const triggerField = this.getNodeParameter('watchField') as EntryType;
const workspaceId = this.getNodeParameter('workspaceId');
if (!webhookData.userId) {
// Cache the user-id that we do not have to request it every time
const userInfo: IUserDto = await clockifyApiRequest.call(this, 'GET', 'user');
webhookData.userId = userInfo.id;
}
const qs: IDataObject = {};
let resource: string;
let result = null;
switch (triggerField) {
case EntryTypes.NEW_TIME_ENTRY:
default:
const workflowTimezone = this.getTimezone();
resource = `workspaces/${workspaceId}/user/${webhookData.userId}/time-entries`;
qs.start = webhookData.lastTimeChecked;
qs.end = moment().tz(workflowTimezone).format('YYYY-MM-DDTHH:mm:ss') + 'Z';
qs.hydrated = true;
qs['in-progress'] = false;
break;
}
result = await clockifyApiRequest.call(this, 'GET', resource, {}, qs);
webhookData.lastTimeChecked = qs.end;
if (Array.isArray(result) && result.length !== 0) {
return [this.helpers.returnJsonArray(result)];
}
return null;
}
}
@@ -0,0 +1,52 @@
export interface IHourlyRateDto {
amount: number;
currency: string;
}
const MembershipStatuses = {
PENDING: 'PENDING',
ACTIVE: 'ACTIVE',
DECLINED: 'DECLINED',
INACTIVE: 'INACTIVE',
} as const;
type MembershipStatusEnum = (typeof MembershipStatuses)[keyof typeof MembershipStatuses];
const TaskStatuses = {
ACTIVE: 'ACTIVE',
DONE: 'DONE',
} as const;
type TaskStatusEnum = (typeof TaskStatuses)[keyof typeof TaskStatuses];
export interface IMembershipDto {
hourlyRate: IHourlyRateDto;
membershipStatus: MembershipStatusEnum;
membershipType: string;
targetId: string;
userId: string;
}
export interface ITagDto {
id: string;
name: any;
workspaceId: string;
archived: boolean;
}
export interface ITaskDto {
assigneeIds: object;
estimate: string;
id: string;
name: any;
workspaceId: string;
projectId: string;
'is-active': boolean;
status: TaskStatusEnum;
}
export interface ITimeIntervalDto {
duration: string;
end: string;
start: string;
}
@@ -0,0 +1,5 @@
export const EntryTypes = {
NEW_TIME_ENTRY: 0,
} as const;
export type EntryType = (typeof EntryTypes)[keyof typeof EntryTypes];
@@ -0,0 +1,66 @@
import type {
IExecuteFunctions,
ILoadOptionsFunctions,
IPollFunctions,
IDataObject,
IHttpRequestMethods,
IRequestOptions,
} from 'n8n-workflow';
export async function clockifyApiRequest(
this: ILoadOptionsFunctions | IPollFunctions | IExecuteFunctions,
method: IHttpRequestMethods,
resource: string,
body: any = {},
qs: IDataObject = {},
_uri?: string,
_option: IDataObject = {},
): Promise<any> {
const BASE_URL = 'https://api.clockify.me/api/v1';
const options: IRequestOptions = {
headers: {
'Content-Type': 'application/json',
},
method,
qs,
body,
uri: `${BASE_URL}/${resource}`,
json: true,
useQuerystring: true,
};
return await this.helpers.requestWithAuthentication.call(this, 'clockifyApi', options);
}
export async function clockifyApiRequestAllItems(
this: IExecuteFunctions | IPollFunctions | ILoadOptionsFunctions,
method: IHttpRequestMethods,
endpoint: string,
body: any = {},
query: IDataObject = {},
): Promise<any> {
const returnData: IDataObject[] = [];
let responseData;
query['page-size'] = 50;
query.page = 1;
do {
responseData = await clockifyApiRequest.call(this, method, endpoint, body, query);
returnData.push.apply(returnData, responseData as IDataObject[]);
const limit = query.limit as number | undefined;
if (limit && returnData.length >= limit) {
return returnData;
}
query.page++;
} while (responseData.length !== 0);
return returnData;
}
@@ -0,0 +1,489 @@
import type { INodeProperties } from 'n8n-workflow';
export const projectOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['project'],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a project',
action: 'Create a project',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a project',
action: 'Delete a project',
},
{
name: 'Get',
value: 'get',
description: 'Get a project',
action: 'Get a project',
},
{
name: 'Get Many',
value: 'getAll',
description: 'Get many projects',
action: 'Get many projects',
},
{
name: 'Update',
value: 'update',
description: 'Update a project',
action: 'Update a project',
},
],
default: 'create',
},
];
export const projectFields: INodeProperties[] = [
/* -------------------------------------------------------------------------- */
/* project:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Project Name',
name: 'name',
type: 'string',
required: true,
default: '',
description: 'Name of project being created',
displayOptions: {
show: {
resource: ['project'],
operation: ['create'],
},
},
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
operation: ['create'],
resource: ['project'],
},
},
default: {},
options: [
{
displayName: 'Billable',
name: 'billable',
type: 'boolean',
default: true,
},
{
displayName: 'Color',
name: 'color',
type: 'color',
default: '#0000FF',
},
{
displayName: 'Client Name or ID',
name: 'clientId',
type: 'options',
description:
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
typeOptions: {
loadOptionsDependsOn: ['workspaceId'],
loadOptionsMethod: 'loadClientsForWorkspace',
},
default: '',
},
{
displayName: 'Estimate',
name: 'estimateUi',
placeholder: 'Add Estimate',
type: 'fixedCollection',
default: {},
typeOptions: {
multipleValues: false,
},
options: [
{
displayName: 'Estimate',
name: 'estimateValues',
values: [
{
displayName: 'Estimate',
name: 'estimate',
type: 'number',
default: 0,
},
{
displayName: 'Type',
name: 'type',
type: 'options',
options: [
{
name: 'Auto',
value: 'AUTO',
},
{
name: 'Manual',
value: 'MANUAL',
},
],
default: 'AUTO',
},
],
},
],
},
{
displayName: 'Is Public',
name: 'isPublic',
type: 'boolean',
default: true,
},
{
displayName: 'Note',
name: 'note',
type: 'string',
default: '',
description: 'Note about the project',
},
],
},
/* -------------------------------------------------------------------------- */
/* project:delete */
/* -------------------------------------------------------------------------- */
{
displayName: 'Project ID',
name: 'projectId',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: ['project'],
operation: ['delete'],
},
},
},
/* -------------------------------------------------------------------------- */
/* project:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Project ID',
name: 'projectId',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: ['project'],
operation: ['get'],
},
},
},
/* -------------------------------------------------------------------------- */
/* project:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
operation: ['getAll'],
resource: ['project'],
},
},
default: false,
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
operation: ['getAll'],
resource: ['project'],
returnAll: [false],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 100,
description: 'Max number of results to return',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
operation: ['getAll'],
resource: ['project'],
},
},
default: {},
options: [
{
displayName: 'Archived',
name: 'archived',
type: 'boolean',
default: true,
},
{
displayName: 'Billable',
name: 'billable',
type: 'boolean',
default: true,
},
{
displayName: 'Client Names or IDs',
name: 'clients',
type: 'multiOptions',
description:
'Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
typeOptions: {
loadOptionsDependsOn: ['workspaceId'],
loadOptionsMethod: 'loadClientsForWorkspace',
},
default: [],
},
{
displayName: 'Contains Client',
name: 'contains-client',
type: 'boolean',
default: false,
description: 'Whether to return only projects having a client',
},
{
displayName: 'Client Status',
name: 'client-status',
type: 'options',
options: [
{
name: 'Active',
value: 'ACTIVE',
},
{
name: 'Archived',
value: 'ARCHIVED',
},
],
default: '',
description: 'If provided, projects will be filtered by whether they have a client',
},
{
displayName: 'Contains User',
name: 'contains-user',
type: 'boolean',
default: false,
description: 'Whether to return only projects having users',
},
{
displayName: 'Is Template',
name: 'is-template',
type: 'boolean',
default: false,
description: 'Whether to return only projects as templates',
},
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
},
{
displayName: 'Sort Column',
name: 'sort-column',
type: 'options',
options: [
{
name: 'Name',
value: 'NAME',
},
{
name: 'Client Name',
value: 'CLIENT_NAME',
},
{
name: 'Duration',
value: 'DURATION',
},
],
default: '',
},
{
displayName: 'Sort Order',
name: 'sort-order',
type: 'options',
options: [
{
name: 'Ascending',
value: 'ASCENDING',
},
{
name: 'Descending',
value: 'DESCENDING',
},
],
default: '',
},
{
displayName: 'User Name or ID',
name: 'users',
type: 'options',
description:
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
typeOptions: {
loadOptionsDependsOn: ['workspaceId'],
loadOptionsMethod: 'loadUsersForWorkspace',
},
default: '',
},
{
displayName: 'User Status',
name: 'user-status',
type: 'options',
options: [
{
name: 'Active',
value: 'ACTIVE',
},
{
name: 'Archived',
value: 'ARCHIVED',
},
],
default: '',
description: 'If provided, projects will be filtered by whether they have a client',
},
],
},
/* -------------------------------------------------------------------------- */
/* project:update */
/* -------------------------------------------------------------------------- */
{
displayName: 'Project ID',
name: 'projectId',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: ['project'],
operation: ['update'],
},
},
},
{
displayName: 'Update Fields',
name: 'updateFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
operation: ['update'],
resource: ['project'],
},
},
default: {},
options: [
{
displayName: 'Billable',
name: 'billable',
type: 'boolean',
default: true,
},
{
displayName: 'Color',
name: 'color',
type: 'color',
default: '#0000FF',
},
{
displayName: 'Client Name or ID',
name: 'clientId',
type: 'options',
description:
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
typeOptions: {
loadOptionsDependsOn: ['workspaceId'],
loadOptionsMethod: 'loadClientsForWorkspace',
},
default: '',
},
{
displayName: 'Estimate',
name: 'estimateUi',
placeholder: 'Add Estimate',
type: 'fixedCollection',
default: {},
typeOptions: {
multipleValues: false,
},
options: [
{
displayName: 'Estimate',
name: 'estimateValues',
values: [
{
displayName: 'Estimate',
name: 'estimate',
type: 'number',
default: 0,
},
{
displayName: 'Type',
name: 'type',
type: 'options',
options: [
{
name: 'Auto',
value: 'AUTO',
},
{
name: 'Manual',
value: 'MANUAL',
},
],
default: 'AUTO',
},
],
},
],
},
{
displayName: 'Is Public',
name: 'isPublic',
type: 'boolean',
default: false,
},
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
},
{
displayName: 'Note',
name: 'note',
type: 'string',
default: '',
description: 'Note about the project',
},
],
},
];
@@ -0,0 +1,60 @@
import type { IHourlyRateDto, IMembershipDto } from './CommonDtos';
const Estimates = {
AUTO: 'AUTO',
MANUAL: 'MANUAL',
} as const;
type EstimateEnum = (typeof Estimates)[keyof typeof Estimates];
interface IEstimateDto {
estimate: string;
type: EstimateEnum;
}
export interface IProjectDto {
archived: boolean;
billable: boolean;
clientId: string;
clientName: string | undefined;
color: string;
duration: string | undefined;
estimate: IEstimateDto | undefined;
hourlyRate: IHourlyRateDto | undefined;
id: string;
memberships: IMembershipDto[] | undefined;
name: string;
isPublic: boolean;
workspaceId: string;
note: string | undefined;
}
export interface IProjectRequest {
name: string;
clientId: string;
isPublic: boolean;
estimate: IEstimateDto;
color: string;
note: string;
billable: boolean;
hourlyRate: IHourlyRateDto;
memberships: IMembershipDto;
tasks: ITaskDto;
}
const TaskStatuses = {
ACTIVE: 'ACTIVE',
DONE: 'DONE',
} as const;
type TaskStatusEnum = (typeof TaskStatuses)[keyof typeof TaskStatuses];
export interface ITaskDto {
assigneeIds: object;
estimate: string;
id: string;
name: string;
projectId: string;
status: TaskStatusEnum;
'is-active': boolean;
}
@@ -0,0 +1,211 @@
import type { INodeProperties } from 'n8n-workflow';
export const tagOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['tag'],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a tag',
action: 'Create a tag',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a tag',
action: 'Delete a tag',
},
{
name: 'Get Many',
value: 'getAll',
description: 'Get many tags',
action: 'Get many tags',
},
{
name: 'Update',
value: 'update',
description: 'Update a tag',
action: 'Update a tag',
},
],
default: 'create',
},
];
export const tagFields: INodeProperties[] = [
/* -------------------------------------------------------------------------- */
/* tag:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Name',
name: 'name',
type: 'string',
required: true,
default: '',
description: 'Name of tag being created',
displayOptions: {
show: {
resource: ['tag'],
operation: ['create'],
},
},
},
/* -------------------------------------------------------------------------- */
/* tag:delete */
/* -------------------------------------------------------------------------- */
{
displayName: 'Tag ID',
name: 'tagId',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: ['tag'],
operation: ['delete'],
},
},
},
/* -------------------------------------------------------------------------- */
/* tag:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
operation: ['getAll'],
resource: ['tag'],
},
},
default: false,
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
operation: ['getAll'],
resource: ['tag'],
returnAll: [false],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 100,
description: 'Max number of results to return',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
operation: ['getAll'],
resource: ['tag'],
},
},
default: {},
options: [
{
displayName: 'Archived',
name: 'archived',
type: 'boolean',
default: true,
},
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
},
{
displayName: 'Sort Column',
name: 'sort-column',
type: 'options',
options: [
{
name: 'Name',
value: 'NAME',
},
],
default: '',
},
{
displayName: 'Sort Order',
name: 'sort-order',
type: 'options',
options: [
{
name: 'Ascending',
value: 'ASCENDING',
},
{
name: 'Descending',
value: 'DESCENDING',
},
],
default: '',
},
],
},
/* -------------------------------------------------------------------------- */
/* tag:update */
/* -------------------------------------------------------------------------- */
{
displayName: 'Tag ID',
name: 'tagId',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: ['tag'],
operation: ['update'],
},
},
},
{
displayName: 'Update Fields',
name: 'updateFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
operation: ['update'],
resource: ['tag'],
},
},
default: {},
options: [
{
displayName: 'Archived',
name: 'archived',
type: 'boolean',
default: false,
},
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
},
],
},
];
@@ -0,0 +1,321 @@
import type { INodeProperties } from 'n8n-workflow';
export const taskOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['task'],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a task',
action: 'Create a task',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a task',
action: 'Delete a task',
},
{
name: 'Get',
value: 'get',
description: 'Get a task',
action: 'Get a task',
},
{
name: 'Get Many',
value: 'getAll',
description: 'Get many tasks',
action: 'Get many tasks',
},
{
name: 'Update',
value: 'update',
description: 'Update a task',
action: 'Update a task',
},
],
default: 'create',
},
];
export const taskFields: INodeProperties[] = [
{
displayName: 'Project Name or ID',
name: 'projectId',
type: 'options',
description:
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
typeOptions: {
loadOptionsDependsOn: ['workspaceId'],
loadOptionsMethod: 'loadProjectsForWorkspace',
},
displayOptions: {
show: {
resource: ['task'],
},
},
required: true,
default: '',
},
/* -------------------------------------------------------------------------- */
/* task:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Task Name',
name: 'name',
type: 'string',
required: true,
default: '',
description: 'Name of task to create',
displayOptions: {
show: {
resource: ['task'],
operation: ['create'],
},
},
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
operation: ['create'],
resource: ['task'],
},
},
default: {},
options: [
{
displayName: 'Assignee Names or IDs',
name: 'assigneeIds',
type: 'multiOptions',
description:
'Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
default: [],
typeOptions: {
loadOptionsMethod: 'loadUsersForWorkspace',
},
},
{
displayName: 'Estimate',
name: 'estimate',
type: 'string',
default: '',
placeholder: '2:30',
description: 'Estimate time the task will take, e.x: 2:30 (2 hours and 30 minutes)',
},
],
},
/* -------------------------------------------------------------------------- */
/* task:delete */
/* -------------------------------------------------------------------------- */
{
displayName: 'Task ID',
name: 'taskId',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: ['task'],
operation: ['delete'],
},
},
description: 'ID of task to delete',
},
/* -------------------------------------------------------------------------- */
/* task:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Task ID',
name: 'taskId',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: ['task'],
operation: ['get'],
},
},
description: 'ID of task to get',
},
/* -------------------------------------------------------------------------- */
/* task:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
operation: ['getAll'],
resource: ['task'],
},
},
default: false,
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
operation: ['getAll'],
resource: ['task'],
returnAll: [false],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 100,
description: 'Max number of results to return',
},
{
displayName: 'Filters',
name: 'filters',
type: 'collection',
placeholder: 'Add Filter',
displayOptions: {
show: {
operation: ['getAll'],
resource: ['task'],
},
},
default: {},
options: [
{
displayName: 'Is Active',
name: 'is-active',
type: 'boolean',
default: false,
},
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
description: 'Text to match in the task name',
},
{
displayName: 'Sort Column',
name: 'sort-column',
type: 'options',
options: [
{
name: 'Name',
value: 'NAME',
},
],
default: 'NAME',
},
{
displayName: 'Sort Order',
name: 'sort-order',
type: 'options',
options: [
{
name: 'Ascending',
value: 'ASCENDING',
},
{
name: 'Descending',
value: 'DESCENDING',
},
],
default: 'ASCENDING',
},
],
},
/* -------------------------------------------------------------------------- */
/* task:update */
/* -------------------------------------------------------------------------- */
{
displayName: 'Task ID',
name: 'taskId',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: ['task'],
operation: ['update'],
},
},
description: 'ID of task to update',
},
{
displayName: 'Update Fields',
name: 'updateFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
operation: ['update'],
resource: ['task'],
},
},
default: {},
options: [
{
displayName: 'Assignee Names or IDs',
name: 'assigneeIds',
type: 'multiOptions',
description:
'Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
default: [],
typeOptions: {
loadOptionsMethod: 'loadUsersForWorkspace',
},
},
{
displayName: 'Estimate',
name: 'estimate',
type: 'string',
default: '',
placeholder: '2:30',
description: 'Estimate time the task will take, e.x: 2:30 (2 hours and 30 minutes)',
},
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
},
{
displayName: 'Status',
name: 'status',
type: 'options',
options: [
{
name: 'Active',
value: 'ACTIVE',
},
{
name: 'Done',
value: 'DONE',
},
],
default: 'ACTIVE',
},
],
},
];
@@ -0,0 +1,349 @@
import type { INodeProperties } from 'n8n-workflow';
export const timeEntryOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['timeEntry'],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a time entry',
action: 'Create a time entry',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a time entry',
action: 'Delete a time entry',
},
{
name: 'Get',
value: 'get',
description: 'Get time entrie',
action: 'Get a time entry',
},
{
name: 'Update',
value: 'update',
description: 'Update a time entry',
action: 'Update a time entry',
},
],
default: 'create',
},
];
export const timeEntryFields: INodeProperties[] = [
/* -------------------------------------------------------------------------- */
/* timeEntry:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Start',
name: 'start',
type: 'dateTime',
required: true,
default: '',
displayOptions: {
show: {
resource: ['timeEntry'],
operation: ['create'],
},
},
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
operation: ['create'],
resource: ['timeEntry'],
},
},
default: {},
options: [
{
displayName: 'Billable',
name: 'billable',
type: 'boolean',
default: false,
},
{
displayName: 'Custom Fields',
name: 'customFieldsUi',
placeholder: 'Add Custom Field',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
description: 'Filter by custom fields',
default: {},
options: [
{
name: 'customFieldsValues',
displayName: 'Custom Field',
values: [
{
displayName: 'Field Name or ID',
name: 'customFieldId',
type: 'options',
typeOptions: {
loadOptionsDependsOn: ['workspaceId'],
loadOptionsMethod: 'loadCustomFieldsForWorkspace',
},
default: '',
description:
'The ID of the field to add custom field to. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'The value to set on custom field',
},
],
},
],
},
{
displayName: 'Description',
name: 'description',
type: 'string',
default: '',
},
{
displayName: 'End',
name: 'end',
type: 'dateTime',
default: '',
},
{
displayName: 'Project Name or ID',
name: 'projectId',
type: 'options',
description:
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
typeOptions: {
loadOptionsDependsOn: ['workspaceId'],
loadOptionsMethod: 'loadProjectsForWorkspace',
},
default: '',
},
{
displayName: 'Tag Names or IDs',
name: 'tagIds',
type: 'multiOptions',
description:
'Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
typeOptions: {
loadOptionsDependsOn: ['workspaceId'],
loadOptionsMethod: 'loadTagsForWorkspace',
},
default: [],
},
{
displayName: 'Task ID',
name: 'taskId',
type: 'string',
default: '',
},
],
},
/* -------------------------------------------------------------------------- */
/* timeEntry:delete */
/* -------------------------------------------------------------------------- */
{
displayName: 'Time Entry ID',
name: 'timeEntryId',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: ['timeEntry'],
operation: ['delete'],
},
},
},
/* -------------------------------------------------------------------------- */
/* timeEntry:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Time Entry ID',
name: 'timeEntryId',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: ['timeEntry'],
operation: ['get'],
},
},
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
operation: ['get'],
resource: ['timeEntry'],
},
},
default: {},
options: [
{
displayName: 'Consider Duration Format',
name: 'consider-duration-format',
type: 'boolean',
default: false,
description:
"Whether to return the time entry's duration rounded to minutes or seconds based on duration format (hh:mm or hh:mm:ss) from workspace settings",
},
{
displayName: 'Hydrated',
name: 'hydrated',
type: 'boolean',
default: false,
description:
"Whether to return the time entry's project, task and tags in full and not just their IDs",
},
],
},
/* -------------------------------------------------------------------------- */
/* timeEntry:update */
/* -------------------------------------------------------------------------- */
{
displayName: 'Time Entry ID',
name: 'timeEntryId',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: ['timeEntry'],
operation: ['update'],
},
},
},
{
displayName: 'Update Fields',
name: 'updateFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
operation: ['update'],
resource: ['timeEntry'],
},
},
default: {},
options: [
{
displayName: 'Billable',
name: 'billable',
type: 'boolean',
default: false,
},
{
displayName: 'Custom Fields',
name: 'customFieldsUi',
placeholder: 'Add Custom Field',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
description: 'Filter by custom fields',
default: {},
options: [
{
name: 'customFieldsValues',
displayName: 'Custom Field',
values: [
{
displayName: 'Field Name or ID',
name: 'customFieldId',
type: 'options',
typeOptions: {
loadOptionsDependsOn: ['workspaceId'],
loadOptionsMethod: 'loadCustomFieldsForWorkspace',
},
default: '',
description:
'The ID of the field to add custom field to. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'The value to set on custom field',
},
],
},
],
},
{
displayName: 'Description',
name: 'description',
type: 'string',
default: '',
},
{
displayName: 'End',
name: 'end',
type: 'dateTime',
default: '',
},
{
displayName: 'Project Name or ID',
name: 'projectId',
type: 'options',
description:
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
typeOptions: {
loadOptionsDependsOn: ['workspaceId'],
loadOptionsMethod: 'loadProjectsForWorkspace',
},
default: '',
},
{
displayName: 'Start',
name: 'start',
type: 'dateTime',
default: '',
},
{
displayName: 'Tag Names or IDs',
name: 'tagIds',
type: 'multiOptions',
description:
'Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
typeOptions: {
loadOptionsDependsOn: ['workspaceId'],
loadOptionsMethod: 'loadTagsForWorkspace',
},
default: [],
},
{
displayName: 'Task ID',
name: 'taskId',
type: 'string',
default: '',
},
],
},
];
@@ -0,0 +1,34 @@
import type { ITimeIntervalDto } from './CommonDtos';
interface ITimeEntriesDurationRequest {
start: string;
end: string;
}
export interface ITimeEntryRequest {
id: string;
start: string;
billable: boolean;
description: string;
projectId: string;
userId: string;
taskId?: string | null;
end: string;
tagIds?: string[] | undefined;
timeInterval: ITimeEntriesDurationRequest;
workspaceId: string;
isLocked: boolean;
}
export interface ITimeEntryDto {
billable: boolean;
description: string;
id: string;
isLocked: boolean;
projectId: string;
tagIds: string[];
taskId: string;
timeInterval: ITimeIntervalDto;
userId: string;
workspaceId: string;
}
@@ -0,0 +1,155 @@
import type { INodeProperties } from 'n8n-workflow';
export const userOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['user'],
},
},
options: [
{
name: 'Get Many',
value: 'getAll',
description: 'Get many users',
action: 'Get many users',
},
],
default: 'getAll',
},
];
export const userFields: INodeProperties[] = [
/* -------------------------------------------------------------------------- */
/* user:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
operation: ['getAll'],
resource: ['user'],
},
},
default: false,
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
operation: ['getAll'],
resource: ['user'],
returnAll: [false],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 100,
description: 'Max number of results to return',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
resource: ['user'],
operation: ['getAll'],
},
},
default: {},
options: [
{
displayName: 'Email',
name: 'email',
type: 'string',
placeholder: 'name@email.com',
default: '',
description:
"If provided, you'll get a filtered list of users that contain the provided string in their email address",
},
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
description:
"If provided, you'll get a filtered list of users that contain the provided string in their name",
},
{
displayName: 'Status',
name: 'status',
type: 'options',
options: [
{
name: 'Active',
value: 'ACTIVE',
},
{
name: 'Inactive',
value: 'INACTIVE',
},
{
name: 'Pending',
value: 'PENDING',
},
{
name: 'Declined',
value: 'DECLINED',
},
],
default: '',
description:
"If provided, you'll get a filtered list of users with the corresponding status",
},
{
displayName: 'Sort Column',
name: 'sort-column',
type: 'options',
options: [
{
name: 'Email',
value: 'EMAIL',
},
{
name: 'Name',
value: 'NAME',
},
{
name: 'Hourly Rate',
value: 'HOURLYRATE',
},
],
default: '',
},
{
displayName: 'Sort Order',
name: 'sort-order',
type: 'options',
options: [
{
name: 'Ascending',
value: 'ASCENDING',
},
{
name: 'Descending',
value: 'DESCENDING',
},
],
default: '',
},
],
},
];
@@ -0,0 +1,23 @@
import type { IDataObject } from 'n8n-workflow';
import type { IMembershipDto } from './CommonDtos';
const UserStatuses = {
ACTIVE: 0,
PENDING_EMAIL_VERIFICATION: 1,
DELETED: 2,
};
export type UserStatusEnum = (typeof UserStatuses)[keyof typeof UserStatuses];
export interface IUserDto {
activeWorkspace: string;
defaultWorkspace: string;
email: string;
id: string;
memberships: IMembershipDto[];
name: string;
profilePicture: string;
settings: IDataObject;
status: UserStatusEnum;
}
@@ -0,0 +1,90 @@
import type { IHourlyRateDto, IMembershipDto } from './CommonDtos';
export const AdminOnlyPages = {
PROJECT: 'PROJECT',
TEAM: 'TEAM',
REPORTS: 'REPORTS',
} as const;
export type AdminOnlyPagesEnum = (typeof AdminOnlyPages)[keyof typeof AdminOnlyPages];
export const DaysOfWeek = {
MONDAY: 'MONDAY',
TUESDAY: 'TUESDAY',
WEDNESDAY: 'WEDNESDAY',
THURSDAY: 'THURSDAY',
FRIDAY: 'FRIDAY',
SATURDAY: 'SATURDAY',
SUNDAY: 'SUNDAY',
} as const;
export type DaysOfWeekEnum = (typeof DaysOfWeek)[keyof typeof DaysOfWeek];
export const DatePeriods = {
DAYS: 'DAYS',
WEEKS: 'WEEKS',
MONTHS: 'MONTHS',
} as const;
export type DatePeriodEnum = (typeof DatePeriods)[keyof typeof DatePeriods];
export const AutomaticLockTypes = {
WEEKLY: 'WEEKLY',
MONTHLY: 'MONTHLY',
OLDER_THAN: 'OLDER_THAN',
} as const;
export type AutomaticLockTypeEnum = (typeof AutomaticLockTypes)[keyof typeof AutomaticLockTypes];
interface IAutomaticLockDto {
changeDay: DaysOfWeekEnum;
dayOfMonth: number;
firstDay: DaysOfWeekEnum;
olderThanPeriod: DatePeriodEnum;
olderThanValue: number;
type: AutomaticLockTypeEnum;
}
interface IRound {
minutes: string;
round: string;
}
interface IWorkspaceSettingsDto {
adminOnlyPages: AdminOnlyPagesEnum[];
automaticLock: IAutomaticLockDto;
canSeeTimeSheet: boolean;
defaultBillableProjects: boolean;
forceDescription: boolean;
forceProjects: boolean;
forceTags: boolean;
forceTasks: boolean;
lockTimeEntries: string;
onlyAdminsCreateProject: boolean;
onlyAdminsCreateTag: boolean;
onlyAdminsSeeAllTimeEntries: boolean;
onlyAdminsSeeBillableRates: boolean;
onlyAdminsSeeDashboard: boolean;
onlyAdminsSeePublicProjectsEntries: boolean;
projectFavorites: boolean;
projectGroupingLabel: string;
projectPickerSpecialFilter: boolean;
round: IRound;
timeRoundingInReports: boolean;
trackTimeDownToSecond: boolean;
}
export interface IWorkspaceDto {
hourlyRate: IHourlyRateDto;
id: string;
imageUrl: string;
memberships: IMembershipDto[];
name: string;
workspaceSettings: IWorkspaceSettingsDto;
}
export interface IClientDto {
id: string;
name: string;
workspaceId: string;
}
@@ -0,0 +1,58 @@
import type { INodeProperties } from 'n8n-workflow';
export const workspaceOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['workspace'],
},
},
options: [
{
name: 'Get Many',
value: 'getAll',
description: 'Get many workspaces',
action: 'Get many workspaces',
},
],
default: 'getAll',
},
];
export const workspaceFields: INodeProperties[] = [
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
operation: ['getAll'],
resource: ['workspace'],
},
},
default: false,
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
operation: ['getAll'],
resource: ['workspace'],
returnAll: [false],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 100,
description: 'Max number of results to return',
},
];
@@ -0,0 +1,24 @@
{
"type": "object",
"properties": {
"archived": {
"type": "boolean"
},
"currencyCode": {
"type": "string"
},
"currencyId": {
"type": "string"
},
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"workspaceId": {
"type": "string"
}
},
"version": 1
}
@@ -0,0 +1,104 @@
{
"type": "object",
"properties": {
"archived": {
"type": "boolean"
},
"billable": {
"type": "boolean"
},
"clientId": {
"type": "string"
},
"clientName": {
"type": "string"
},
"color": {
"type": "string"
},
"costRate": {
"type": "null"
},
"duration": {
"type": "string"
},
"estimate": {
"type": "object",
"properties": {
"estimate": {
"type": "string"
},
"type": {
"type": "string"
}
}
},
"hourlyRate": {
"type": "object",
"properties": {
"amount": {
"type": "integer"
},
"currency": {
"type": "string"
}
}
},
"id": {
"type": "string"
},
"memberships": {
"type": "array",
"items": {
"type": "object",
"properties": {
"costRate": {
"type": "null"
},
"membershipStatus": {
"type": "string"
},
"membershipType": {
"type": "string"
},
"targetId": {
"type": "string"
},
"userId": {
"type": "string"
}
}
}
},
"name": {
"type": "string"
},
"public": {
"type": "boolean"
},
"template": {
"type": "boolean"
},
"timeEstimate": {
"type": "object",
"properties": {
"active": {
"type": "boolean"
},
"estimate": {
"type": "string"
},
"includeNonBillable": {
"type": "boolean"
},
"type": {
"type": "string"
}
}
},
"workspaceId": {
"type": "string"
}
},
"version": 1
}
@@ -0,0 +1,110 @@
{
"type": "object",
"properties": {
"archived": {
"type": "boolean"
},
"billable": {
"type": "boolean"
},
"clientId": {
"type": "string"
},
"clientName": {
"type": "string"
},
"color": {
"type": "string"
},
"estimate": {
"type": "object",
"properties": {
"estimate": {
"type": "string"
},
"type": {
"type": "string"
}
}
},
"estimateReset": {
"type": "null"
},
"hourlyRate": {
"type": "object",
"properties": {
"amount": {
"type": "integer"
},
"currency": {
"type": "string"
}
}
},
"id": {
"type": "string"
},
"memberships": {
"type": "array",
"items": {
"type": "object",
"properties": {
"costRate": {
"type": "null"
},
"hourlyRate": {
"type": "null"
},
"membershipStatus": {
"type": "string"
},
"membershipType": {
"type": "string"
},
"targetId": {
"type": "string"
},
"userId": {
"type": "string"
}
}
}
},
"name": {
"type": "string"
},
"note": {
"type": "string"
},
"public": {
"type": "boolean"
},
"template": {
"type": "boolean"
},
"timeEstimate": {
"type": "object",
"properties": {
"active": {
"type": "boolean"
},
"estimate": {
"type": "string"
},
"includeNonBillable": {
"type": "boolean"
},
"resetOption": {
"type": "null"
},
"type": {
"type": "string"
}
}
},
"workspaceId": {
"type": "string"
}
},
"version": 1
}
@@ -0,0 +1,58 @@
{
"type": "object",
"properties": {
"billable": {
"type": "boolean"
},
"customFieldValues": {
"type": "array",
"items": {
"type": "object",
"properties": {
"customFieldId": {
"type": "string"
},
"name": {
"type": "string"
},
"timeEntryId": {
"type": "string"
},
"type": {
"type": "string"
},
"value": {
"type": "null"
}
}
}
},
"id": {
"type": "string"
},
"isLocked": {
"type": "boolean"
},
"kioskId": {
"type": "null"
},
"timeInterval": {
"type": "object",
"properties": {
"start": {
"type": "string"
}
}
},
"type": {
"type": "string"
},
"userId": {
"type": "string"
},
"workspaceId": {
"type": "string"
}
},
"version": 1
}
@@ -0,0 +1,138 @@
{
"type": "object",
"properties": {
"activeWorkspace": {
"type": "string"
},
"customFields": {
"type": "array",
"items": {
"type": "object",
"properties": {
"customFieldId": {
"type": "string"
},
"customFieldName": {
"type": "string"
},
"customFieldType": {
"type": "string"
},
"userId": {
"type": "string"
}
}
}
},
"defaultWorkspace": {
"type": "string"
},
"email": {
"type": "string"
},
"id": {
"type": "string"
},
"settings": {
"type": "object",
"properties": {
"alerts": {
"type": "boolean"
},
"approval": {
"type": "boolean"
},
"collapseAllProjectLists": {
"type": "boolean"
},
"dashboardPinToTop": {
"type": "boolean"
},
"dashboardSelection": {
"type": "string"
},
"dashboardViewType": {
"type": "string"
},
"dateFormat": {
"type": "string"
},
"groupSimilarEntriesDisabled": {
"type": "boolean"
},
"isCompactViewOn": {
"type": "boolean"
},
"lang": {
"type": "string"
},
"longRunning": {
"type": "boolean"
},
"multiFactorEnabled": {
"type": "boolean"
},
"myStartOfDay": {
"type": "string"
},
"onboarding": {
"type": "boolean"
},
"projectPickerTaskFilter": {
"type": "boolean"
},
"pto": {
"type": "boolean"
},
"reminders": {
"type": "boolean"
},
"scheduledReports": {
"type": "boolean"
},
"scheduling": {
"type": "boolean"
},
"sendNewsletter": {
"type": "boolean"
},
"showOnlyWorkingDays": {
"type": "boolean"
},
"summaryReportSettings": {
"type": "object",
"properties": {
"group": {
"type": "string"
},
"subgroup": {
"type": "string"
}
}
},
"theme": {
"type": "string"
},
"timeFormat": {
"type": "string"
},
"timeTrackingManual": {
"type": "boolean"
},
"timeZone": {
"type": "string"
},
"weeklyUpdates": {
"type": "boolean"
},
"weekStart": {
"type": "string"
}
}
},
"status": {
"type": "string"
}
},
"version": 1
}
@@ -0,0 +1,4 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M25.8005 13.7484L34.9844 4.55872L37.6133 7.18731L28.4293 16.3772L25.8005 13.7484ZM22.0791 23.1398C20.2818 23.1398 18.8257 21.6773 18.8257 19.8714C18.8257 18.0673 20.2818 16.6029 22.0791 16.6029C23.8765 16.6029 25.3326 18.0673 25.3326 19.8714C25.3326 21.6773 23.8765 23.1398 22.0791 23.1398ZM37.6562 32.632L35.0274 35.2625L25.8433 26.0725L28.4722 23.442L37.6562 32.632Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M22.1739 34.0058C24.0252 34.0058 25.7889 33.6364 27.4055 32.9797L31.8911 37.4703C29.0144 39.0764 25.7031 40 22.1739 40C11.1366 40 2.1875 31.0452 2.1875 20.0009C2.1875 8.95484 11.1367 0 22.1739 0C25.6678 0 28.9492 0.901094 31.8053 2.4775L27.3942 6.89328C25.7797 6.24031 24.0216 5.87094 22.1739 5.87094C14.4442 5.87094 8.17781 12.1692 8.17781 19.9394C8.17781 27.7075 14.4441 34.0058 22.1739 34.0058Z" fill="#03A9F4"/>
</svg>

After

Width:  |  Height:  |  Size: 1002 B

@@ -0,0 +1,4 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M25.8005 13.7484L34.9844 4.55872L37.6133 7.18731L28.4293 16.3772L25.8005 13.7484ZM22.0791 23.1398C20.2818 23.1398 18.8257 21.6773 18.8257 19.8714C18.8257 18.0673 20.2818 16.6029 22.0791 16.6029C23.8765 16.6029 25.3326 18.0673 25.3326 19.8714C25.3326 21.6773 23.8765 23.1398 22.0791 23.1398ZM37.6562 32.632L35.0274 35.2625L25.8433 26.0725L28.4722 23.442L37.6562 32.632Z" fill="#222222"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M22.1739 34.0058C24.0252 34.0058 25.7889 33.6364 27.4055 32.9797L31.8911 37.4703C29.0144 39.0764 25.7031 40 22.1739 40C11.1366 40 2.1875 31.0452 2.1875 20.0009C2.1875 8.95484 11.1367 0 22.1739 0C25.6678 0 28.9492 0.901094 31.8053 2.4775L27.3942 6.89328C25.7797 6.24031 24.0216 5.87094 22.1739 5.87094C14.4442 5.87094 8.17781 12.1692 8.17781 19.9394C8.17781 27.7075 14.4441 34.0058 22.1739 34.0058Z" fill="#03A9F4"/>
</svg>

After

Width:  |  Height:  |  Size: 1004 B