Skip to content

Commit 4c93726

Browse files
authored
Merge pull request #4 from bufferapp/fix-resource-property
Add backwards compatibility for legacy GBP fields
2 parents 6dc91b8 + d5f12f9 commit 4c93726

5 files changed

Lines changed: 141 additions & 78 deletions

File tree

‎nodes/Buffer/__tests__/google-business.test.ts‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,37 @@ describe('Buffer Node - Create Post', () => {
253253
}),
254254
).rejects.toThrow('Google Business posts do not support video attachments');
255255
});
256+
257+
// Workflows saved before "Specify Event Time" replaced the old "Full Day
258+
// Event" toggle still have a `googleEventIsFullDay` value stored on the node,
259+
// even though it's no longer part of this node's UI. These tests simulate
260+
// that legacy saved shape to make sure old timed events don't silently
261+
// become all-day events after upgrading.
262+
describe('legacy googleEventIsFullDay workflows', () => {
263+
it('should send the full start/end date-time and keep isFullDayEvent false for a legacy timed event', async () => {
264+
const input = await executePostCreate({
265+
...googleEventDefaults,
266+
googleEventIsFullDay: false,
267+
});
268+
const meta = (input.metadata as IDataObject).google as IDataObject;
269+
const details = meta.detailsEvent as IDataObject;
270+
expect(details.startDate).toBe('2026-07-01T10:00:00.000Z');
271+
expect(details.endDate).toBe('2026-07-01T18:00:00.000Z');
272+
expect(details.isFullDayEvent).toBe(false);
273+
});
274+
275+
it('should send the full start/end date-time and keep isFullDayEvent true for a legacy all-day event', async () => {
276+
const input = await executePostCreate({
277+
...googleEventDefaults,
278+
googleEventIsFullDay: true,
279+
});
280+
const meta = (input.metadata as IDataObject).google as IDataObject;
281+
const details = meta.detailsEvent as IDataObject;
282+
expect(details.startDate).toBe('2026-07-01T10:00:00.000Z');
283+
expect(details.endDate).toBe('2026-07-01T18:00:00.000Z');
284+
expect(details.isFullDayEvent).toBe(true);
285+
});
286+
});
256287
});
257288

258289
// -------------------------------------------------------

‎nodes/Buffer/__tests__/test-helpers.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ export async function executePostCreate(params: Record<string, unknown>) {
2323

2424
const mockContext = {
2525
getInputData: () => [{ json: {} }],
26-
getNodeParameter: (name: string) => {
26+
getNodeParameter: (name: string, _itemIndex: number, fallback?: unknown) => {
2727
if (!(name in defaults)) {
28-
return '';
28+
return fallback !== undefined ? fallback : '';
2929
}
3030
return defaults[name];
3131
},
@@ -103,10 +103,10 @@ export async function runExecute(
103103

104104
const mockContext = {
105105
getInputData: () => itemsDefaults.map(() => ({ json: {} })),
106-
getNodeParameter: (name: string, itemIndex: number) => {
106+
getNodeParameter: (name: string, itemIndex: number, fallback?: unknown) => {
107107
const defaults = itemsDefaults[itemIndex];
108108
if (!defaults || !(name in defaults)) {
109-
return '';
109+
return fallback !== undefined ? fallback : '';
110110
}
111111
return defaults[name];
112112
},

‎nodes/Buffer/actions/post/networks/google-business.ts‎

Lines changed: 43 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -46,42 +46,62 @@ export function buildGoogleBusinessMetadata(ctx: IExecuteFunctions, itemIndex: n
4646
const title = ctx.getNodeParameter('googleEventTitle', itemIndex) as string;
4747
const startDate = ctx.getNodeParameter('googleEventStartDate', itemIndex) as string;
4848
const endDate = ctx.getNodeParameter('googleEventEndDate', itemIndex) as string;
49-
const hasTime = ctx.getNodeParameter('googleEventHasTime', itemIndex) as boolean;
5049
const button = ctx.getNodeParameter('googleEventButton', itemIndex) as string;
5150
const link = ctx.getNodeParameter('googleEventLink', itemIndex) as string;
5251

52+
// Workflows saved before "Specify Event Time" replaced the old "Full Day Event"
53+
// toggle still carry a `googleEventIsFullDay` value in their stored parameters,
54+
// even though that field no longer exists in this node's UI. The old behavior
55+
// always sent the full start/end date-time and used this flag only to set
56+
// isFullDayEvent, so replicate that exactly here rather than falling through to
57+
// the new hasTime logic - otherwise old timed events would silently become
58+
// all-day events (isFullDayEvent would flip from false to true) after upgrading.
59+
const legacyIsFullDay = ctx.getNodeParameter('googleEventIsFullDay', itemIndex, null) as
60+
| boolean
61+
| null;
62+
5363
let eventStartDate: string;
5464
let eventEndDate: string;
55-
if (hasTime) {
56-
const startTime = ctx.getNodeParameter('googleEventStartTime', itemIndex) as string;
57-
const endTime = ctx.getNodeParameter('googleEventEndTime', itemIndex) as string;
58-
if (!startTime || !endTime) {
59-
throw new NodeOperationError(
60-
ctx.getNode(),
61-
'Please provide both an "Event Start Time" and "Event End Time", or disable "Specify Event Time" to create an all-day event.',
62-
{ itemIndex },
63-
);
64-
}
65-
if (!TIME_FORMAT_REGEX.test(startTime) || !TIME_FORMAT_REGEX.test(endTime)) {
66-
throw new NodeOperationError(
67-
ctx.getNode(),
68-
'Event times must be in 24-hour HH:mm format, e.g. 14:30.',
69-
{ itemIndex },
70-
);
71-
}
72-
eventStartDate = combineDateAndTime(startDate, startTime);
73-
eventEndDate = combineDateAndTime(endDate, endTime);
65+
let isFullDayEvent: boolean;
66+
67+
if (legacyIsFullDay !== null) {
68+
eventStartDate = new Date(startDate).toISOString();
69+
eventEndDate = new Date(endDate).toISOString();
70+
isFullDayEvent = legacyIsFullDay;
7471
} else {
75-
eventStartDate = toDateOnlyIso(startDate);
76-
eventEndDate = toDateOnlyIso(endDate);
72+
const hasTime = ctx.getNodeParameter('googleEventHasTime', itemIndex) as boolean;
73+
if (hasTime) {
74+
const startTime = ctx.getNodeParameter('googleEventStartTime', itemIndex) as string;
75+
const endTime = ctx.getNodeParameter('googleEventEndTime', itemIndex) as string;
76+
if (!startTime || !endTime) {
77+
throw new NodeOperationError(
78+
ctx.getNode(),
79+
'Please provide both an "Event Start Time" and "Event End Time", or disable "Specify Event Time" to create an all-day event.',
80+
{ itemIndex },
81+
);
82+
}
83+
if (!TIME_FORMAT_REGEX.test(startTime) || !TIME_FORMAT_REGEX.test(endTime)) {
84+
throw new NodeOperationError(
85+
ctx.getNode(),
86+
'Event times must be in 24-hour HH:mm format, e.g. 14:30.',
87+
{ itemIndex },
88+
);
89+
}
90+
eventStartDate = combineDateAndTime(startDate, startTime);
91+
eventEndDate = combineDateAndTime(endDate, endTime);
92+
} else {
93+
eventStartDate = toDateOnlyIso(startDate);
94+
eventEndDate = toDateOnlyIso(endDate);
95+
}
96+
isFullDayEvent = !hasTime;
7797
}
7898

7999
if (title) googleMeta.title = title;
80100
const details: IDataObject = {
81101
title,
82102
startDate: eventStartDate,
83103
endDate: eventEndDate,
84-
isFullDayEvent: !hasTime,
104+
isFullDayEvent,
85105
};
86106
if (button) details.button = button;
87107
if (link) details.link = link;

‎nodes/Buffer/descriptions/index.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { INodeProperties } from 'n8n-workflow';
22
import { ideaProperties } from './idea.description';
3-
import { postCoreProperties, postAttachmentProperties, postAssetProperties } from './post.description';
3+
import { resourceProperty, postCoreProperties, postAttachmentProperties, postAssetProperties } from './post.description';
44
import { googleBusinessProperties } from './google-business.description';
55
import { pinterestProperties } from './pinterest.description';
66
import { facebookProperties } from './facebook.description';
@@ -9,7 +9,10 @@ import { youtubeProperties } from './youtube.description';
99

1010
// Concatenated in the same order the fields originally appeared in Buffer.node.ts's `properties`
1111
// array, so the field order in the n8n UI is unchanged by the split into per-resource/network files.
12+
// The Resource selector must come first, since the Idea/Post fields that follow are conditionally
13+
// shown based on its value.
1214
export const properties: INodeProperties[] = [
15+
...resourceProperty,
1316
...ideaProperties,
1417
...postCoreProperties,
1518
...googleBusinessProperties,

‎nodes/Buffer/descriptions/post.description.ts‎

Lines changed: 59 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import type { INodeProperties } from 'n8n-workflow';
22

3-
// Resource, Post's Operation field, core post fields (channel/text/scheduling), and both
4-
// schedulingType field variants (see the comment on the field below for why there are two).
5-
export const postCoreProperties: INodeProperties[] = [
6-
// ----------------------------------
7-
// Resources
8-
// ----------------------------------
3+
// The Resource selector, kept as its own export so it can be placed first in the concatenated
4+
// properties array (see descriptions/index.ts) — it must come before the Idea/Post-specific
5+
// fields it controls, otherwise those fields render above it once "Idea" (the default) is
6+
// selected, pushing the selector out of its expected position in the n8n UI.
7+
export const resourceProperty: INodeProperties[] = [
98
{
109
displayName: 'Resource',
1110
name: 'resource',
@@ -23,6 +22,11 @@ export const postCoreProperties: INodeProperties[] = [
2322
],
2423
default: 'idea',
2524
},
25+
];
26+
27+
// Post's Operation field, core post fields (channel/text/scheduling), and both schedulingType
28+
// field variants (see the comment on the field below for why there are two).
29+
export const postCoreProperties: INodeProperties[] = [
2630
// ----------------------------------
2731
// Operations
2832
// ----------------------------------
@@ -236,30 +240,20 @@ export const postCoreProperties: INodeProperties[] = [
236240
},
237241
];
238242

239-
// The four attachmentType field variants: generic (hidden for YouTube/Google Business/Pinterest),
240-
// YouTube-only (forces 'video'), Google Business-only (no video option), and Pinterest-only
241-
// (forces 'image').
243+
// The four attachmentType field variants: YouTube-only (forces 'video'), Google Business-only
244+
// (no video option), Pinterest-only (forces 'image'), and generic (hidden for those three
245+
// channels). The generic variant is deliberately listed LAST: n8n resolves a duplicate-named
246+
// field's initial default (before an org/channel has been selected, so channelService is empty
247+
// and no displayOptions match yet) from the LAST matching property definition in the array,
248+
// not by evaluating displayOptions. Listing the generic 'none' variant last ensures a freshly
249+
// added node defaults to "No Attachment" instead of whichever network-specific variant happens
250+
// to be last.
242251
export const postAttachmentProperties: INodeProperties[] = [
243252
{
244-
// YouTube posts are always videos, Google Business does not support video, and
245-
// Pinterest posts are always images, so this field is hidden for those channels and
246-
// replaced with the network-specific fields below (see the Scheduling Mode fields
247-
// above for why this uses field-level displayOptions rather than per-option
248-
// displayOptions).
249253
displayName: 'Attachment Type',
250254
name: 'attachmentType',
251255
type: 'options',
252256
options: [
253-
{
254-
name: 'No Attachment',
255-
value: 'none',
256-
description: 'Create a text-only post',
257-
},
258-
{
259-
name: 'Image',
260-
value: 'image',
261-
description: 'Create a post with an image',
262-
},
263257
{
264258
name: 'Video',
265259
value: 'video',
@@ -270,49 +264,43 @@ export const postAttachmentProperties: INodeProperties[] = [
270264
show: {
271265
resource: ['post'],
272266
operation: ['create'],
273-
},
274-
hide: {
275-
channelService: [
276-
'youtube', 'YouTube', 'YOUTUBE',
277-
'google', 'Google', 'GOOGLE', 'googlebusiness', 'GoogleBusiness', 'GOOGLEBUSINESS', 'google_business', 'Google_Business', 'GOOGLE_BUSINESS',
278-
'pinterest', 'Pinterest', 'PINTEREST',
279-
],
267+
channelService: ['youtube', 'YouTube', 'YOUTUBE'],
280268
},
281269
},
282-
default: 'none',
283-
description: 'Type of attachment to add to the post',
270+
default: 'video',
271+
description: 'YouTube posts only support video attachments',
284272
},
285273
{
286274
displayName: 'Attachment Type',
287275
name: 'attachmentType',
288276
type: 'options',
289277
options: [
290278
{
291-
name: 'Video',
292-
value: 'video',
293-
description: 'Create a post with a video',
279+
name: 'No Attachment',
280+
value: 'none',
281+
description: 'Create a text-only post',
282+
},
283+
{
284+
name: 'Image',
285+
value: 'image',
286+
description: 'Create a post with an image',
294287
},
295288
],
296289
displayOptions: {
297290
show: {
298291
resource: ['post'],
299292
operation: ['create'],
300-
channelService: ['youtube', 'YouTube', 'YOUTUBE'],
293+
channelService: ['google', 'Google', 'GOOGLE', 'googlebusiness', 'GoogleBusiness', 'GOOGLEBUSINESS', 'google_business', 'Google_Business', 'GOOGLE_BUSINESS'],
301294
},
302295
},
303-
default: 'video',
304-
description: 'YouTube posts only support video attachments',
296+
default: 'none',
297+
description: 'Google Business posts do not support video attachments',
305298
},
306299
{
307300
displayName: 'Attachment Type',
308301
name: 'attachmentType',
309302
type: 'options',
310303
options: [
311-
{
312-
name: 'No Attachment',
313-
value: 'none',
314-
description: 'Create a text-only post',
315-
},
316304
{
317305
name: 'Image',
318306
value: 'image',
@@ -323,32 +311,53 @@ export const postAttachmentProperties: INodeProperties[] = [
323311
show: {
324312
resource: ['post'],
325313
operation: ['create'],
326-
channelService: ['google', 'Google', 'GOOGLE', 'googlebusiness', 'GoogleBusiness', 'GOOGLEBUSINESS', 'google_business', 'Google_Business', 'GOOGLE_BUSINESS'],
314+
channelService: ['pinterest', 'Pinterest', 'PINTEREST'],
327315
},
328316
},
329-
default: 'none',
330-
description: 'Google Business posts do not support video attachments',
317+
default: 'image',
318+
description: 'Pinterest posts always require an image attachment',
331319
},
332320
{
321+
// YouTube posts are always videos, Google Business does not support video, and
322+
// Pinterest posts are always images, so this field is hidden for those channels and
323+
// replaced with the network-specific fields above (see the Scheduling Mode fields
324+
// above for why this uses field-level displayOptions rather than per-option
325+
// displayOptions).
333326
displayName: 'Attachment Type',
334327
name: 'attachmentType',
335328
type: 'options',
336329
options: [
330+
{
331+
name: 'No Attachment',
332+
value: 'none',
333+
description: 'Create a text-only post',
334+
},
337335
{
338336
name: 'Image',
339337
value: 'image',
340338
description: 'Create a post with an image',
341339
},
340+
{
341+
name: 'Video',
342+
value: 'video',
343+
description: 'Create a post with a video',
344+
},
342345
],
343346
displayOptions: {
344347
show: {
345348
resource: ['post'],
346349
operation: ['create'],
347-
channelService: ['pinterest', 'Pinterest', 'PINTEREST'],
350+
},
351+
hide: {
352+
channelService: [
353+
'youtube', 'YouTube', 'YOUTUBE',
354+
'google', 'Google', 'GOOGLE', 'googlebusiness', 'GoogleBusiness', 'GOOGLEBUSINESS', 'google_business', 'Google_Business', 'GOOGLE_BUSINESS',
355+
'pinterest', 'Pinterest', 'PINTEREST',
356+
],
348357
},
349358
},
350-
default: 'image',
351-
description: 'Pinterest posts always require an image attachment',
359+
default: 'none',
360+
description: 'Type of attachment to add to the post',
352361
},
353362
];
354363

0 commit comments

Comments
 (0)