Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
2ae4cc3
fix(app-headless-cms): refresh revision list after create/delete (#5614)
brunozoric Aug 28, 2026
1dbd1e5
fix(app-headless-cms): revision number (#5617)
brunozoric Aug 28, 2026
4d6371c
fix(app-headless-cms): revision list actions (#5616)
brunozoric Aug 28, 2026
dede7ef
feat(app-headless-cms): show created-by and published-by on revision …
brunozoric Aug 28, 2026
4818e1c
feat(app-headless-cms): add unpublish action to revision list
brunozoric Aug 28, 2026
d71bff6
feat(app-headless-cms): show alert when viewing an older revision
brunozoric Aug 28, 2026
49326df
feat(app-headless-cms): add visible status label to revision list items
brunozoric Aug 28, 2026
afb2113
feat(app-headless-cms): add/edit revision note from revision list
brunozoric Aug 28, 2026
5dcdd51
fix(app-headless-cms): navigate to new revision after creating from l…
brunozoric Aug 28, 2026
32b505d
feat(app-headless-cms): show last-published date on Live column with …
brunozoric Aug 28, 2026
0978d1d
test(api-headless-cms): prove updateRevisionDescription wipes latest …
brunozoric Aug 28, 2026
6a2b943
fix(api-headless-cms): add updateRevision storage op to prevent lates…
brunozoric Aug 28, 2026
7129dc5
fix: implement updateRevision in DDB-ES and SQL backends, fix CellLiv…
brunozoric Aug 31, 2026
1dad7ad
fix(app-headless-cms): register missing dependencies in RevisionsList…
brunozoric Aug 31, 2026
35f162e
feat(app-headless-cms-scheduler): show last-published date in schedul…
brunozoric Aug 31, 2026
01ef735
fix(app-headless-cms): prevent revision drawer from closing when a di…
brunozoric Aug 31, 2026
fcb4e92
test(api-headless-cms): prove live field not cleared after deleting p…
brunozoric Aug 31, 2026
b922fc7
fix(api-headless-cms): clear live field when deleting a published rev…
brunozoric Aug 31, 2026
751c194
fix(api-headless-cms): handle null flags in pattern validator
brunozoric Sep 4, 2026
6376945
fix(api-headless-cms): preserve error data and stack in crud layer
brunozoric Sep 4, 2026
2782d76
feat(app-headless-cms): enable sorting on Live column by lastPublishedOn
brunozoric Sep 7, 2026
f484694
fix(app-headless-cms-scheduler): add sortable to scheduler Live colum…
brunozoric Sep 7, 2026
1fb0d86
feat(app-scheduler): show timezone on schedule date picker
brunozoric Sep 7, 2026
0af42b3
feat(admin-ui): add showTimezone prop to DatePicker
brunozoric Sep 7, 2026
92c9822
feat(app-headless-cms-scheduler): show timezone in scheduled action m…
brunozoric Sep 7, 2026
78516c7
feat(app-scheduler): show timezone in rescheduling alert
brunozoric Sep 7, 2026
e17d58c
feat(app-scheduler): use dateTimeTz picker with timezone selector
brunozoric Sep 7, 2026
5d81a88
fix(admin-ui): handle Date objects in dateTimeTz picker helpers
brunozoric Sep 7, 2026
ea5f6ad
refactor(admin-ui): extract timezone helpers and emit UTC from dateTi…
brunozoric Sep 7, 2026
cd41f9c
fix(admin-ui): keep UTC value when switching timezone in dateTimeTz p…
brunozoric Sep 7, 2026
31af436
fix(app-scheduler): use consistent UTC offset format across all date …
brunozoric Sep 7, 2026
d9ef692
fix(app-headless-cms): move last-published date to tooltip on Live co…
brunozoric Sep 7, 2026
3590c87
refactor: move formatUtcOffset to app-admin and add timezone to Live …
brunozoric Sep 7, 2026
ac79d28
feat(app-headless-cms): add publish action to revision list drawer
brunozoric Sep 8, 2026
05d3d5d
fix(app-headless-cms): fix type error in publishRevision confirmation
brunozoric Sep 8, 2026
c538d6c
fix(app-headless-cms): update form entry meta after publish/unpublish…
brunozoric Sep 8, 2026
503b2b9
fix: forward patchEntryMeta in form presenter decorators
brunozoric Sep 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion packages/admin-ui/src/DatePicker/DatePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,22 @@ import {
FormComponentLabel,
FormComponentNote
} from "~/FormComponent/index.js";
import { Text } from "~/Text/index.js";

type DatePickerProps = DatePickerPrimitiveProps & FormComponentProps;
interface DatePickerOwnProps {
showTimezone?: boolean;
}

type DatePickerProps = DatePickerPrimitiveProps & FormComponentProps & DatePickerOwnProps;

const TimezoneLabel = () => {
const timezone = useMemo(() => Intl.DateTimeFormat().resolvedOptions().timeZone, []);
return (
<Text size={"sm"} className={"text-neutral-strong mt-xxs"}>
Timezone: {timezone}
</Text>
);
};

const DecoratableDatePicker = ({
label,
Expand All @@ -20,6 +34,7 @@ const DecoratableDatePicker = ({
required,
disabled,
validation,
showTimezone = false,
...props
}: DatePickerProps) => {
const { isValid: validationIsValid, message: validationMessage } = validation || {};
Expand All @@ -40,6 +55,7 @@ const DecoratableDatePicker = ({
disabled={disabled}
invalid={invalid}
/>
{showTimezone && <TimezoneLabel />}
<FormComponentErrorMessage
text={validationMessage}
invalid={invalid}
Expand Down
43 changes: 19 additions & 24 deletions packages/admin-ui/src/DatePicker/primitives/DateTimePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import {
formatDateForDisplay,
formatTimeValue,
getLocalTimezone,
parseTimeValue,
toIsoWithTz
parseTimeValue
} from "../utils/dateHelpers.js";
import { naiveDateToUtcIso, parseToDate, utcToTimezoneDate } from "../utils/timezoneHelpers.js";
import { DatePickerTrigger } from "./components/DatePickerTrigger.js";
import { TimePicker } from "./components/TimePicker.js";

Expand All @@ -22,17 +22,6 @@ type DateTimePickerInternalProps = (DateTimeLocalPickerProps | DateTimeTzPickerP

const timezoneOptions = UTC_TIMEZONES.map(tz => ({ value: tz.value, label: tz.label }));

function parseToDate(value: string | undefined): Date | undefined {
if (!value) {
return undefined;
}
const d = new Date(value);
if (Number.isNaN(d.getTime())) {
return undefined;
}
return d;
}

const DateTimePicker = ({
value,
onChange,
Expand All @@ -51,7 +40,7 @@ const DateTimePicker = ({
}: DateTimePickerInternalProps) => {
const [open, setOpen] = useState(false);

const existingTz = withTimezone && value ? extractTimezone(value as string) : undefined;
const existingTz = withTimezone && value ? extractTimezone(value) : undefined;
const [timezone, setTimezone] = useState(existingTz || getLocalTimezone());

const handleOpenChange = (isOpen: boolean) => {
Expand All @@ -63,18 +52,27 @@ const DateTimePicker = ({

const currentDate = parseToDate(value);

const displayDate =
withTimezone && currentDate ? utcToTimezoneDate(currentDate, timezone) : currentDate;

const displayValue = withTimezone
? formatDateForDisplay(value, "dateTimeTz", displayFormat)
? displayDate
? formatDateForDisplay(
format(displayDate, "yyyy-MM-dd'T'HH:mm:ss") + timezone,
"dateTimeTz",
displayFormat
)
: undefined
: formatDateForDisplay(value, "dateTimeLocal", displayFormat);

const timeValue = currentDate ? formatTimeValue(currentDate) : "";
const timeValue = displayDate ? formatTimeValue(displayDate) : "";

const emitChange = (date: Date, tz?: string) => {
if (!onChange) {
return;
}
if (withTimezone) {
onChange(toIsoWithTz(date, tz || timezone));
onChange(naiveDateToUtcIso(date, tz || timezone));
} else {
onChange(format(date, "yyyy-MM-dd'T'HH:mm:ss") + ".000Z");
}
Expand All @@ -84,8 +82,8 @@ const DateTimePicker = ({
if (!date) {
return;
}
if (currentDate) {
date.setHours(currentDate.getHours(), currentDate.getMinutes());
if (displayDate) {
date.setHours(displayDate.getHours(), displayDate.getMinutes());
}
emitChange(date);
};
Expand All @@ -98,16 +96,13 @@ const DateTimePicker = ({
if (!parsed) {
return;
}
const base = currentDate ? new Date(currentDate) : new Date();
const base = displayDate ? new Date(displayDate) : new Date();
base.setHours(parsed.hours, parsed.minutes, 0, 0);
emitChange(base);
};

const handleTimezoneChange = (tz: string) => {
setTimezone(tz);
if (currentDate) {
emitChange(currentDate, tz);
}
};

return (
Expand All @@ -125,7 +120,7 @@ const DateTimePicker = ({
<div className="flex flex-col gap-sm">
<Calendar
mode="single"
selected={currentDate}
selected={displayDate}
onSelect={handleDateSelect}
weekStartsOn={weekStartsOn}
disabled={[
Expand Down
7 changes: 5 additions & 2 deletions packages/admin-ui/src/DatePicker/utils/dateHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export function formatDateForDisplay(
if (!value) {
return undefined;
}
const isoStr = value as string;
const isoStr = value instanceof Date ? value.toISOString() : (value as string);
const tzMatch = isoStr.match(/([+-]\d{2}:\d{2})$/);
const tz = tzMatch ? `UTC${tzMatch[1]}` : "";
const dateWithoutTz = isoStr.replace(/[+-]\d{2}:\d{2}$/, "");
Expand Down Expand Up @@ -164,7 +164,10 @@ export function getLocalTimezone(): string {
return `${sign}${hours}:${minutes}`;
}

export function extractTimezone(isoString: string): string | undefined {
export function extractTimezone(isoString: string | Date | unknown): string | undefined {
if (typeof isoString !== "string") {
return undefined;
}
const match = isoString.match(/([+-]\d{2}:\d{2})$/);
if (match) {
return match[1];
Expand Down
45 changes: 45 additions & 0 deletions packages/admin-ui/src/DatePicker/utils/timezoneHelpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { format } from "date-fns";

interface ParsedOffset {
sign: number;
minutes: number;
}

function parseOffset(tz: string): ParsedOffset | null {
const match = tz.match(/^([+-])(\d{2}):(\d{2})$/);
if (!match) {
return null;
}
const sign = match[1] === "+" ? 1 : -1;
return { sign, minutes: sign * (Number(match[2]) * 60 + Number(match[3])) };
}

export function utcToTimezoneDate(utcDate: Date, timezone: string): Date {
const parsed = parseOffset(timezone);
if (!parsed) {
return utcDate;
}
const localOffsetMinutes = -utcDate.getTimezoneOffset();
const shiftMs = (parsed.minutes - localOffsetMinutes) * 60 * 1000;
return new Date(utcDate.getTime() + shiftMs);
}

export function naiveDateToUtcIso(date: Date, timezone: string): string {
const naive = format(date, "yyyy-MM-dd'T'HH:mm:ss");
const parsed = parseOffset(timezone);
if (parsed) {
return new Date(`${naive}${timezone}`).toISOString();
}
return date.toISOString();
}

export function parseToDate(value: string | Date | undefined): Date | undefined {
if (!value) {
return undefined;
}
const d = new Date(value);
if (Number.isNaN(d.getTime())) {
return undefined;
}
return d;
}
47 changes: 47 additions & 0 deletions packages/api-headless-cms-ddb-es/src/operations/entry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,52 @@ export const createEntriesStorageOperations = (
return initialStorageEntry;
};

const updateRevision: CmsEntryStorageOperations["updateRevision"] = async (
initialModel,
params
) => {
const { entry: initialEntry, storageEntry: initialStorageEntry } = params;
const model = getStorageOperationsModel(initialModel);

const transformer = createTransformer({
valuesModifiers,
model,
entry: initialEntry,
storageEntry: initialStorageEntry,
fieldIndexRegistry,
compressionHandler
});

const { storageEntry } = transformer.transformEntryKeys();
const revisionKeys = createEntryRevisionKeys(storageEntry);

const entityBatch = entity.createEntityWriter({
put: [
{
...revisionKeys,
data: storageEntry
}
]
});

try {
await entityBatch.execute();
dataLoaders.clearAll({
tenant: initialEntry.tenant
});
return initialStorageEntry;
} catch (ex) {
throw new WebinyError(
ex.message || "Could not update revision.",
ex.code || "UPDATE_REVISION_ERROR",
{
error: ex,
entry: initialEntry
}
);
}
};

const move: CmsEntryStorageOperations["move"] = async (
initialModel: CmsModel,
id: string,
Expand Down Expand Up @@ -2169,6 +2215,7 @@ export const createEntriesStorageOperations = (
create,
createRevisionFrom,
update,
updateRevision,
move,
delete: deleteEntry,
moveToBin,
Expand Down
40 changes: 40 additions & 0 deletions packages/api-headless-cms-ddb/src/operations/entry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,45 @@ export const createEntriesStorageOperations = (
}
};

const updateRevision: CmsEntryStorageOperations["updateRevision"] = async (
initialModel,
params
) => {
const { entry, storageEntry: initialStorageEntry } = params;
const model = getStorageOperationsModel(initialModel);

const storageEntry = convertToStorageEntry({
model,
storageEntry: initialStorageEntry
});

const entityBatch = entity.createEntityWriter({
put: [
{
...createEntryRevisionKeys(storageEntry),
data: storageEntry
}
]
});

try {
await entityBatch.execute();
dataLoaders.clearAll({
tenant: entry.tenant
});
return initialStorageEntry;
} catch (ex) {
throw new WebinyError(
ex.message || "Could not update revision.",
ex.code || "UPDATE_REVISION_ERROR",
{
error: ex,
entry
}
);
}
};

const move: CmsEntryStorageOperations["move"] = async (initialModel, id, folderId) => {
/**
* We need to:
Expand Down Expand Up @@ -1387,6 +1426,7 @@ export const createEntriesStorageOperations = (
create,
createRevisionFrom,
update,
updateRevision,
move,
delete: deleteEntry,
moveToBin,
Expand Down
19 changes: 19 additions & 0 deletions packages/api-headless-cms-sql/src/operations/entry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,25 @@ export const createEntriesStorageOperations = (
return entry;
},

updateRevision: async (model, { entry, storageEntry }) => {
await entryTableManager.ensureTable();

const existing = await query().where("id", storageEntry.id).first();
const se = storageEntry as CmsStorageEntry;
se.isLatest = existing?.isLatest ?? se.isLatest;
se.isPublished = existing?.isPublished ?? se.isPublished;

const row = entryToRow(se);
const { isLatest: _il, isPublished: _ip, ...rowWithoutFlags } = row;

await query()
.where("tenant", model.tenant)
.andWhere("id", storageEntry.id)
.update(rowWithoutFlags);

return entry;
},

publish: async (model, { entry, storageEntry }) => {
await entryTableManager.ensureTable();

Expand Down
Loading