Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add displayTimezone option to datetime input #8181

Draft
wants to merge 87 commits into
base: next
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
87 commits
Select commit Hold shift + click to select a range
32891e6
feat: add displayTimezone option to datetime input
EoinFalconer Jan 4, 2025
aedccf7
fix: build issue
EoinFalconer Jan 4, 2025
5f1ba30
Merge branch 'next' into feat/SAPP-1970
EoinFalconer Jan 6, 2025
f86aef5
fix: remove need for moment in util package and tooltip
EoinFalconer Jan 9, 2025
3c4c029
Merge branch 'next' into feat/SAPP-1970
EoinFalconer Jan 9, 2025
346cb2b
chore: update pnpm-lock
EoinFalconer Jan 9, 2025
5edbc56
chore: comments
EoinFalconer Jan 9, 2025
aa73651
Merge branch 'next' into feat/SAPP-1970
EoinFalconer Jan 10, 2025
11b05d4
chore: update pnpm-lock
EoinFalconer Jan 10, 2025
aca00c2
Merge branch 'next' into feat/SAPP-1970
EoinFalconer Jan 10, 2025
6bd9c5a
chore: add back sanity client
EoinFalconer Jan 10, 2025
6e48550
chore: fix formatting
EoinFalconer Jan 10, 2025
bd048cd
Merge branch 'next' into feat/SAPP-1970
EoinFalconer Jan 13, 2025
a2767fa
chore: review updates
EoinFalconer Jan 14, 2025
c6bb474
chore: add example in the datetime schema
EoinFalconer Jan 14, 2025
d297345
fix: build issue
EoinFalconer Jan 14, 2025
3453c18
Merge branch 'next' into feat/SAPP-1970
EoinFalconer Jan 14, 2025
ec50096
refactor: moving calendar inputs from scheduled pub. to `core/compone…
jordanl17 Jan 21, 2025
84dd2ba
chore(structure): remove `TimelineStore` from `<DocumentPaneProvider/…
pedrobonamin Jan 21, 2025
ff56861
Merge branch 'next' into feat/SAPP-1970
EoinFalconer Feb 5, 2025
001c395
fix(structure): avoid spreading `key` into props, pass explicitly (#8…
rexxars Feb 11, 2025
3fb1ac0
fix(deps): update dependency @portabletext/editor to ^1.33.0 (#8596)
renovate[bot] Feb 11, 2025
5c22372
fix(deps): update dependency @sanity/client to ^6.28.0 (#8599)
renovate[bot] Feb 11, 2025
07ae47f
chore(deps): update dependency turbo to ^2.4.1 (#8600)
renovate[bot] Feb 11, 2025
ab6d8b0
fix(docs): replace `previewDrafts` with `drafts` (#8601)
stipsan Feb 12, 2025
9cdfc01
fix(deps): update dependency @portabletext/block-tools to ^1.1.7 (#8604)
renovate[bot] Feb 12, 2025
08bf43f
fix(deps): update dependency @portabletext/editor to ^1.33.1 (#8606)
renovate[bot] Feb 12, 2025
69289ea
fix(deps): Update dev-non-major (#8582)
renovate[bot] Feb 12, 2025
176a56b
feat(core): add default bold/italic markdown shortcuts to PTE (#8602)
christianhg Feb 12, 2025
31408f2
fix(deps): Update dev-non-major (#8608)
renovate[bot] Feb 12, 2025
9e5f70b
test(efps): avoid bolding start and end markers (#8611)
christianhg Feb 12, 2025
cb8a3fa
chore(deps): lock file maintenance (#8613)
renovate[bot] Feb 12, 2025
2706f6f
fix(core): add permissions limitation to document actions in releases…
RitaDias Feb 12, 2025
162dadf
feat: updating document count for release overview cells (#8576)
jordanl17 Feb 12, 2025
a52047d
fix(structure): don't modify EMPTY_PARAMS object (#8605)
mariusGundersen Feb 12, 2025
415eac2
feat(typegen): glob for files synchronously for deterministic result
sgulseth Feb 12, 2025
c02bd51
docs(core): exposing `usePerspective` and updating tsDoc (#8528)
jordanl17 Feb 12, 2025
a954ac2
chore: update social badge in readme (#8617)
bjoerge Feb 13, 2025
778ce0e
fix(core): create new doc button tooltip updated for when perspective…
jordanl17 Feb 13, 2025
9095de8
fix(core): update releases permissions for main release action (#8588)
RitaDias Feb 13, 2025
9391ac6
fix(sanity): apply recursion fix to `deriveSearchWeightsFromType2024`
juice49 Feb 6, 2025
40e1ba6
fix: release action dialog fixes; release overview style (#8619)
jordanl17 Feb 13, 2025
621524d
fix(deps): update dependency @portabletext/editor to ^1.33.2 (#8615)
renovate[bot] Feb 13, 2025
6e63c23
fix(deps): update dependency @sanity/ui to ^2.12.4 (#8624)
renovate[bot] Feb 13, 2025
0324381
chore: use `useEffectEvent` aware linting (#8625)
stipsan Feb 13, 2025
73d6b0e
fix(releases): improvements to archive release detail (#8597)
jordanl17 Feb 13, 2025
441ef20
fix(deps): update dependency @portabletext/editor to ^1.33.3 (#8627)
renovate[bot] Feb 13, 2025
3bb4392
fix(core): in release documents rows only show errors count (#8628)
pedrobonamin Feb 14, 2025
0975011
chore(sanity): update release time tooltip (#8629)
juice49 Feb 14, 2025
ce9e97d
chore: improving type definition for useProjectSubscriptions (#8626)
jordanl17 Feb 14, 2025
53aa0fc
fix(core): releases edit events error when calculating differences (#…
pedrobonamin Feb 14, 2025
9d351fa
fix(core): change publish now release action wording (#8636)
pedrobonamin Feb 14, 2025
15f6746
fix(core): simplify pin release button on release detail screen (#8635)
pedrobonamin Feb 14, 2025
70a58cb
fix(core): add permission limitation for create release (#8623)
RitaDias Feb 14, 2025
e5df01f
refactor(core): use `EventListenerPlugin` in `PortableTextInput` (#8640)
christianhg Feb 14, 2025
b2f2e85
chore(sanity): fix intent params links (#8641)
rcmaples Feb 14, 2025
a4b0a46
fix(deps): update dependency @portabletext/editor to ^1.33.4 (#8642)
renovate[bot] Feb 14, 2025
38cb32a
feat(cli): select org and add to sanity.cli.ts on initialization (#8573)
cngonzalez Feb 14, 2025
10f470f
fix: working but list is janky
EoinFalconer Feb 15, 2025
8a054fb
chore: merge next
EoinFalconer Feb 15, 2025
018281d
fix: fix import
EoinFalconer Feb 15, 2025
fe8b45c
Merge branch 'next' into feat/SAPP-1970
EoinFalconer Feb 16, 2025
85dec2e
chore: update pnpm-lock
EoinFalconer Feb 16, 2025
d134df6
feat: displayTimeZone & allow implemented
EoinFalconer Feb 16, 2025
719fb94
chore: merge next
EoinFalconer Feb 16, 2025
182d2bf
chore: moved useTimeZone to core
EoinFalconer Feb 17, 2025
e3808bc
fix: use correct funcs in cal and fix list
EoinFalconer Feb 17, 2025
d541840
fix: build fixes
EoinFalconer Feb 17, 2025
3f5fd73
fix: fix mocks and add console warning
EoinFalconer Feb 17, 2025
9f29bf3
chore: remove dep
EoinFalconer Feb 17, 2025
9ad5863
Merge branch 'next' into feat/SAPP-1970
EoinFalconer Feb 17, 2025
5385dd1
chore: format
EoinFalconer Feb 17, 2025
8cc283e
chore: update lock file
EoinFalconer Feb 17, 2025
0aadc16
fix: remove useDocumentTitle in core
EoinFalconer Feb 18, 2025
d6f7891
chore(core): update useTimeZone to export const, fix mock path
pedrobonamin Feb 18, 2025
8a8123b
chore: merge next
EoinFalconer Feb 18, 2025
4027709
chore: test fixes
EoinFalconer Feb 18, 2025
3004741
Merge branch 'next' into feat/SAPP-1970
EoinFalconer Feb 18, 2025
78aa98e
fix: tests
EoinFalconer Feb 18, 2025
cafccaf
chore: fix tests
EoinFalconer Feb 19, 2025
69a2883
chore: styling
EoinFalconer Feb 19, 2025
684adfe
chore: add more tests
EoinFalconer Feb 19, 2025
cf1d473
fix: fix dateInput
EoinFalconer Feb 19, 2025
a6ac7e6
Merge branch 'next' into feat/SAPP-1970
EoinFalconer Feb 19, 2025
f78c837
chore: update lock file
EoinFalconer Feb 19, 2025
f8a5d28
chore: format
EoinFalconer Feb 19, 2025
1b5dad6
chore: lint
EoinFalconer Feb 19, 2025
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
2 changes: 1 addition & 1 deletion packages/@repo/tsconfig/base.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"description": "Shared TS config that are used for builds, dts generation, etl extract, and vscode/intellisense",
"compilerOptions": {
// Everything needs to use the same lib settings for `paths` to work consistently throughout the monorepo
"lib": ["dom", "es2020", "ES2022.Error"],
"lib": ["dom", "es2020", "ES2022.Error", "ES2022.Intl"],

// Output settings
"target": "ESNext",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface DatetimeOptions extends BaseSchemaTypeOptions {
dateFormat?: string
timeFormat?: string
timeStep?: number
displayTimezone?: string
}

/** @public */
Expand Down
4 changes: 3 additions & 1 deletion packages/@sanity/util/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,12 @@
"watch": "pkg-utils watch"
},
"dependencies": {
"@date-fns/tz": "^1.2.0",
"@date-fns/utc": "^2.1.0",
"@sanity/client": "^6.24.3",
"@sanity/types": "3.69.0",
"date-fns": "4.1.0",
"get-random-values-esm": "1.0.2",
"moment": "^2.30.1",
"rxjs": "^7.8.1"
},
"devDependencies": {
Expand Down
302 changes: 302 additions & 0 deletions packages/@sanity/util/src/datetime-formatter/formatter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,302 @@
/**
* Returns the long/short/narrow name of the month of `date`.
*/
function getMonthName(
date: Date,
style: 'long' | 'short' | 'narrow' | undefined = 'long',
locale = 'en-US',
): string {
// style can be "long", "short", or "narrow"
return new Intl.DateTimeFormat(locale, {month: style}).format(date)
}

/**
* Returns the long/short/narrow name of the day of week of `date`.
*/
function getDayName(
date: Date,
style: 'long' | 'short' | 'narrow' | undefined = 'long',
locale = 'en-US',
): string {
// style can be "long", "short", or "narrow"
return new Intl.DateTimeFormat(locale, {weekday: style}).format(date)
}

/**
* Zero-pads a number to `length` digits (e.g. zeroPad(7, 2) = "07").
*/
function zeroPad(num: number, length: number): string {
return String(num).padStart(length, '0')
}

/**
* Returns an English ordinal for a given day number
*/
function getOrdinal(day: number): string {
const j = day % 10
const k = day % 100
if (j === 1 && k !== 11) return `${day}st`
if (j === 2 && k !== 12) return `${day}nd`
if (j === 3 && k !== 13) return `${day}rd`
return `${day}th`
}

function getISODayOfWeek(date: Date): number {
// Sunday=0 in JS, but ISO calls Monday=1...Sunday=7
const dow = date.getDay()
return dow === 0 ? 7 : dow
}

function getISOWeekYear(date: Date): number {
// Clone date, shift to the "Thursday" of this week
const temp = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()))
const dayOfWeek = getISODayOfWeek(temp)
temp.setUTCDate(temp.getUTCDate() - dayOfWeek + 4)
return temp.getUTCFullYear()
}

function getISOWeekNumber(date: Date): number {
const temp = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()))
const dayOfWeek = getISODayOfWeek(temp)
temp.setUTCDate(temp.getUTCDate() - dayOfWeek + 4)
const yearStart = new Date(Date.UTC(temp.getUTCFullYear(), 0, 1))
return Math.ceil(((temp.valueOf() - yearStart.valueOf()) / 86400000 + 1) / 7)
}

function getDayOfYear(date: Date): number {
const startOfYear = new Date(Date.UTC(date.getFullYear(), 0, 1))
// fix for local-time differences
const diff =
date.valueOf() -
startOfYear.valueOf() +
(startOfYear.getTimezoneOffset() - date.getTimezoneOffset()) * 60_000
return Math.floor(diff / (1000 * 60 * 60 * 24)) + 1
}

// “Locale” week-year => approximate with ISO logic here
function getLocaleWeekYear(date: Date): number {
return getISOWeekYear(date)
}

/**
* Returns fractional seconds based on the count of 'S' in the token.
*/
function getFractionalSeconds(date: Date, length: number): string {
const ms = zeroPad(date.getMilliseconds(), 3) // "123"
if (length === 1) {
return ms.slice(0, 1) // "1"
} else if (length === 2) {
return ms.slice(0, 2) // "12"
} else if (length === 3) {
return ms // "123"
}
// length=4 => e.g. "1230"
return `${ms}0`
}

/**
* Returns a time zone offset string for the system’s local offset
*/
function getTimeZoneOffsetString(date: Date, colon = true): string {
const offsetMinutes = -date.getTimezoneOffset()
const sign = offsetMinutes >= 0 ? '+' : '-'
const absMinutes = Math.abs(offsetMinutes)
const hh = zeroPad(Math.floor(absMinutes / 60), 2)
const mm = zeroPad(absMinutes % 60, 2)
return colon ? `${sign}${hh}:${mm}` : `${sign}${hh}${mm}`
}

/**
* Formats a Date object using many Moment-like tokens.
*/
function formatMomentLike(date: Date, formatStr: string): string {
// Store escaped sequences to restore later
const escapeSequences: string[] = []
const escapeToken = '\uE000' // Use a Unicode private use character as placeholder

// Replace bracketed content with placeholders
const processedFormat = formatStr.replace(/\[([^\]]+)\]/g, (_, contents) => {
escapeSequences.push(contents)
return escapeToken
})

// Basic fields
const year = date.getFullYear()
const monthIndex = date.getMonth() // 0..11
const dayOfMonth = date.getDate() // 1..31
const dayOfWeek = date.getDay() // 0..6 (Sun=0)
const hours = date.getHours() // 0..23
const minutes = date.getMinutes() // 0..59
const seconds = date.getSeconds() // 0..59

// Week-related
const isoWeekNum = getISOWeekNumber(date)
const isoWeekYear = getISOWeekYear(date)
const localeWeekYear = getLocaleWeekYear(date)

// Timestamps
const unixMs = date.getTime() // milliseconds since epoch
const unixSec = Math.floor(unixMs / 1000) // seconds since epoch

// Build token -> value map
const tokens = [
// Year
// 1970 1971 ... 2029 2030
{key: 'YYYY', value: String(year)},
// 70 71 ... 29 30
{key: 'YY', value: String(year).slice(-2)},
// 1970 1971 ... 9999 +10000 +10001
{key: 'Y', value: String(year)},
// Expanded years, -001970 -001971 ... +001907 +001971
{key: 'YYYYY', value: zeroPad(year, 5)},

// ISO week-year
// 1970 1971 ... 2029 2030
{key: 'GGGG', value: String(isoWeekYear)},
// 70 71 ... 29 30
{key: 'GG', value: String(isoWeekYear).slice(-2)},

// "locale" week-year
{key: 'gggg', value: String(localeWeekYear)},
{key: 'gg', value: String(localeWeekYear).slice(-2)},

// Quarter
{key: 'Q', value: String(Math.floor(monthIndex / 3) + 1)},
{key: 'Qo', value: getOrdinal(Math.floor(monthIndex / 3) + 1)},

// --- Month (using Intl) ---
{key: 'MMMM', value: getMonthName(date, 'long')}, // e.g. "January"
{key: 'MMM', value: getMonthName(date, 'short')}, // e.g. "Jan"
// For numeric months, we still do a manual approach:
{key: 'MM', value: zeroPad(monthIndex + 1, 2)},
{key: 'M', value: String(monthIndex + 1)},
{key: 'Mo', value: getOrdinal(monthIndex + 1)},

// Day of Month
{key: 'DD', value: zeroPad(dayOfMonth, 2)},
{key: 'D', value: String(dayOfMonth)},
{key: 'Do', value: getOrdinal(dayOfMonth)},

// --- Day of Week (using Intl) ---
{key: 'dddd', value: getDayName(date, 'long')}, // e.g. "Monday"
{key: 'ddd', value: getDayName(date, 'short')}, // e.g. "Mon"
{
key: 'dd',
// e.g. "Mo" => first 2 chars of short day name
value: getDayName(date, 'short').slice(0, 2),
},
{key: 'd', value: String(dayOfWeek)},
{key: 'do', value: getOrdinal(dayOfWeek + 1)},

// Day of the year
{key: 'DDDD', value: zeroPad(getDayOfYear(date), 3)},
{key: 'DDD', value: String(getDayOfYear(date))},
{key: 'DDDo', value: getOrdinal(getDayOfYear(date))},

// ISO day of week
{key: 'E', value: String(getISODayOfWeek(date))},

// Day of Year
{key: 'DDDD', value: zeroPad(getDayOfYear(date), 3)},
{key: 'DDD', value: String(getDayOfYear(date))},

// Week of the year
// w 1 2 ... 52 53
{key: 'w', value: zeroPad(isoWeekNum, 2)},
// week 1st 2nd ... 52nd 53rd
{key: 'wo', value: getOrdinal(isoWeekNum)},
// 01 02 ... 52 53
{key: 'ww', value: zeroPad(isoWeekNum, 2)},

// ISO Week
{key: 'WW', value: zeroPad(isoWeekNum, 2)},
{key: 'W', value: String(isoWeekNum)},
{key: 'Wo', value: getOrdinal(isoWeekNum)},

// or "locale" week => replace isoWeekNum

// 24h hours
{key: 'HH', value: zeroPad(hours, 2)},
{key: 'H', value: String(hours)},

// 12h hours
{key: 'hh', value: zeroPad(((hours + 11) % 12) + 1, 2)},
{key: 'h', value: String(((hours + 11) % 12) + 1)},

// 1 2 ... 23 24
{key: 'k', value: String(hours || 24)},
// 01 02 ... 23 24
{key: 'kk', value: zeroPad(hours || 24, 2)},

// Minutes
{key: 'mm', value: zeroPad(minutes, 2)},
{key: 'm', value: String(minutes)},

// Seconds
{key: 'ss', value: zeroPad(seconds, 2)},
{key: 's', value: String(seconds)},

// Fractional seconds (S..SSSS) => handled separately
// Timezone offset (Z, ZZ) => handled separately

// AM/PM
{key: 'A', value: hours < 12 ? 'AM' : 'PM'},
{key: 'a', value: hours < 12 ? 'am' : 'pm'},

// Unix timestamps
{key: 'X', value: String(unixSec)},
{key: 'x', value: String(unixMs)},

// Eras BC AD
{key: 'N', value: year < 0 ? 'BC' : 'AD'},
{key: 'NN', value: year < 0 ? 'BC' : 'AD'},
{key: 'NNN', value: year < 0 ? 'BC' : 'AD'},

// Before Christ, Anno Domini
{key: 'NNNN', value: year < 0 ? 'Before Christ' : 'Anno Domini'},
{key: 'NNNNN', value: year < 0 ? 'BC' : 'AD'},
]

// Sort tokens by descending length to avoid partial collisions
tokens.sort((a, b) => b.key.length - a.key.length)

// 1) Fractional seconds
const fracSecRegex = /(S{1,4})/g
let output = processedFormat.replace(fracSecRegex, (match) => {
return getFractionalSeconds(date, match.length)
})

// 2) Time zone offset (Z, ZZ)
const tzRegex = /(Z{1,2})/g
output = output.replace(tzRegex, (match) => {
// Z => +HH:mm, ZZ => +HHmm
const useColon = match === 'Z'
return getTimeZoneOffsetString(date, useColon)
})

// Time zone EST CST ...MST PST
const tzRegex2 = /(z{1,2})/g
output = output.replace(tzRegex2, (match) => {
// z => EST, zzz => Eastern Standard Time
return Intl.DateTimeFormat('en-US', {
timeZoneName: match.length === 1 ? 'short' : 'long',
}).format(date)
})

// Find each token and replace it, make sure not to replace overlapping tokens

for (const {key, value} of tokens) {
// Escape special characters
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
// Match the token, but only if it’s not part of a larger word
const tokenRegex = new RegExp(`(^|[^A-Z0-9a-z])(${escapedKey})(?![A-Z0-9a-z])`, 'g')
output = output.replace(tokenRegex, `$1${value}`)
}

// After all token replacements, restore escaped sequences
output = output.replace(new RegExp(escapeToken, 'g'), () => escapeSequences.shift() || '')

return output
}

export default formatMomentLike
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* Converts a Moment.js format string into a UTS 35 (Unicode Technical Standard #35)
* format string
*
* This function doesn't take absolutely every token into account, but should cover
* all common cases. If you find a missing token, feel free to add it.
*
*/
function momentToDateFnsFormat(momentFormat: string): string {
// A list of replacements from Moment tokens to date-fns tokens
// ordered from longest to shortest to prevent partial replacements
const formatMap: Record<string, string> = {
YYYY: 'yyyy',
YY: 'yy',
MMMM: 'MMMM',
MMM: 'MMM',
MM: 'MM',
M: 'M',
DD: 'dd',
D: 'd',
dddd: 'EEEE',
ddd: 'EEE',
HH: 'HH',
H: 'H',
hh: 'hh',
h: 'h',
mm: 'mm',
m: 'm',
ss: 'ss',
s: 's',
A: 'a',
a: 'a',
}

// Replace each token in the format string
return Object.keys(formatMap).reduce(
(acc, key) => acc.replace(new RegExp(key, 'g'), formatMap[key]),
momentFormat,
)
}

export default momentToDateFnsFormat
Loading
Loading