Add weekly layout to record calendar (#22819)

## Summary

- Add a week layout to record calendar views and persist the selected
layout.
- Render `DATE` calendars as an all-day week and `DATE_TIME` calendars
as an hourly week.
- Add an optional end date field across calendar configuration,
metadata, persistence, and complete-view upserts.
- Use configured end values for ranged and multi-day events, with a
one-hour fallback when a `DATE_TIME` end is absent or invalid.
- Keep calendar cards consistent with the existing compact view,
including checkbox selection and whole-card record opening.
- Gate the weekly layout and end-date behavior behind the public Labs
`IS_CALENDAR_WEEK_VIEW_ENABLED` workspace feature flag.

## Week interactions

- Show overlapping timed events side by side and cap the visible records
at two per day.
- Display start and end times on timed cards, enforce a readable
30-minute minimum height, and keep today’s text contrast stronger.
- Drag timed events between days and times with 30-minute snapping while
preserving their duration, including zero-duration events.
- Show a create button when hovering a 30-minute slot; keyboard users
can focus a day, move the slot with the arrow keys, and reach the same
contextual action.
- Initialize new records with the selected slot time and a compatible
writable end value one hour later.
- Show the workspace time zone and current-time indicator in timed
weeks; date-only weeks keep the all-day section without an hourly grid.

## Configuration and data loading

- Only allow end fields that match the start field type, and prevent
selecting the same field for both boundaries.
- Load records whose ranges overlap the visible period so month and week
layouts display the same relevant records.
- Resolve and persist calendar end fields when updating existing views
through `upsert_complete_view`.
- Fall back to Month and ignore the configured end field while the flag
is disabled, without overwriting either persisted setting, so
re-enabling restores the previous configuration.
- Expose the flag in Labs and keep it default-off for workspaces without
a stored value; enable it in the development seeder.

<img width="1285" height="808" alt="Screenshot 2026-07-15 at 15 50 17"
src="https://github.com/user-attachments/assets/b7e3f7f1-ca77-492f-8cce-cca186ebca0b"
/>
This commit is contained in:
Weiko
2026-07-15 16:30:18 +02:00
committed by GitHub
parent 0dbae2eda3
commit 25bd2897a3
158 changed files with 5446 additions and 214 deletions
@@ -343,6 +343,7 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma
"anyFieldFilterValue",
"calendarLayout",
"calendarFieldMetadataUniversalIdentifier",
"calendarEndFieldMetadataUniversalIdentifier",
"visibility",
"mainGroupByFieldMetadataUniversalIdentifier",
"shouldHideEmptyGroups",
@@ -69,6 +69,7 @@ exports[`registry-derived override property maps derives the overridable propert
"anyFieldFilterValue",
"calendarLayout",
"calendarFieldMetadataId",
"calendarEndFieldMetadataId",
"visibility",
"mainGroupByFieldMetadataId",
"shouldHideEmptyGroups",
@@ -381,6 +381,12 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
universalProperty: 'calendarFieldMetadataUniversalIdentifier',
isOverridable: true,
},
calendarEndFieldMetadataId: {
toCompare: true,
toStringify: false,
universalProperty: 'calendarEndFieldMetadataUniversalIdentifier',
isOverridable: true,
},
visibility: {
toCompare: true,
toStringify: false,
@@ -98,6 +98,9 @@ export const ALL_MANY_TO_ONE_METADATA_FOREIGN_KEY = {
calendarFieldMetadata: {
foreignKey: 'calendarFieldMetadataId',
},
calendarEndFieldMetadata: {
foreignKey: 'calendarEndFieldMetadataId',
},
kanbanAggregateOperationFieldMetadata: {
foreignKey: 'kanbanAggregateOperationFieldMetadataId',
},
@@ -174,6 +174,13 @@ export const ALL_MANY_TO_ONE_METADATA_RELATIONS = {
isNullable: true,
universalForeignKey: 'calendarFieldMetadataUniversalIdentifier',
},
calendarEndFieldMetadata: {
metadataName: 'fieldMetadata',
foreignKey: 'calendarEndFieldMetadataId',
inverseOneToManyProperty: 'calendarEndViews',
isNullable: true,
universalForeignKey: 'calendarEndFieldMetadataUniversalIdentifier',
},
kanbanAggregateOperationFieldMetadata: {
metadataName: 'fieldMetadata',
foreignKey: 'kanbanAggregateOperationFieldMetadataId',
@@ -71,6 +71,12 @@ export const ALL_ONE_TO_MANY_METADATA_RELATIONS = {
universalFlatEntityForeignKeyAggregator:
'calendarViewUniversalIdentifiers',
},
calendarEndViews: {
metadataName: 'view',
flatEntityForeignKeyAggregator: 'calendarEndViewIds',
universalFlatEntityForeignKeyAggregator:
'calendarEndViewUniversalIdentifiers',
},
mainGroupByFieldMetadataViews: {
metadataName: 'view',
flatEntityForeignKeyAggregator: 'mainGroupByFieldMetadataViewIds',
@@ -17,7 +17,7 @@ type FieldMetadataOneToManySyncableRelations =
type Assertions = [
// FieldMetadataEntity OneToMany relations (all targets):
// - indexFieldMetadatas, fieldPermissions, viewFields, viewFilters,
// - kanbanAggregateOperationViews, calendarViews, mainGroupByFieldMetadataViews,
// - kanbanAggregateOperationViews, calendarViews, calendarEndViews, mainGroupByFieldMetadataViews,
// - viewSorts, searchFieldMetadatas
Expect<
Equal<
@@ -28,6 +28,7 @@ type Assertions = [
| 'viewFilters'
| 'kanbanAggregateOperationViews'
| 'calendarViews'
| 'calendarEndViews'
| 'mainGroupByFieldMetadataViews'
| 'viewSorts'
| 'searchFieldMetadatas'
@@ -43,6 +44,7 @@ type Assertions = [
| 'viewFilters'
| 'kanbanAggregateOperationViews'
| 'calendarViews'
| 'calendarEndViews'
| 'mainGroupByFieldMetadataViews'
| 'viewSorts'
| 'searchFieldMetadatas'
@@ -10,7 +10,7 @@ type FieldMetadataRelatedProperties =
type Assertions = [
// FieldMetadataEntity has both ManyToOne and OneToMany relations
// ManyToOne: object, workspace, application, relationTargetFieldMetadata, relationTargetObjectMetadata
// OneToMany: indexFieldMetadatas, fieldPermissions, viewFields, viewFilters, kanbanAggregateOperationViews, calendarViews, mainGroupByFieldMetadataViews, viewSorts, searchFieldMetadatas
// OneToMany: indexFieldMetadatas, fieldPermissions, viewFields, viewFilters, kanbanAggregateOperationViews, calendarViews, calendarEndViews, mainGroupByFieldMetadataViews, viewSorts, searchFieldMetadatas
Expect<
Equal<
FieldMetadataRelatedProperties,
@@ -25,6 +25,7 @@ type Assertions = [
| 'viewFilters'
| 'kanbanAggregateOperationViews'
| 'calendarViews'
| 'calendarEndViews'
| 'mainGroupByFieldMetadataViews'
| 'viewSorts'
| 'searchFieldMetadatas'
@@ -57,6 +57,8 @@ describe('addFlatEntityToFlatEntityAndRelatedEntityMapsThroughMutationOrThrow',
applicationId,
calendarFieldMetadataId: mockFieldMetadata.id,
calendarFieldMetadataUniversalIdentifier: fieldUniversalIdentifier,
calendarEndFieldMetadataId: mockFieldMetadata.id,
calendarEndFieldMetadataUniversalIdentifier: fieldUniversalIdentifier,
};
const flatEntityAndRelatedMapsToMutate: MetadataFlatEntityAndRelatedFlatEntityMaps<'view'> =
@@ -103,6 +105,8 @@ describe('addFlatEntityToFlatEntityAndRelatedEntityMapsThroughMutationOrThrow',
).toMatchObject<Partial<FlatFieldMetadata>>({
calendarViewIds: [mockView.id],
calendarViewUniversalIdentifiers: [viewUniversalIdentifier],
calendarEndViewIds: [mockView.id],
calendarEndViewUniversalIdentifiers: [viewUniversalIdentifier],
});
});
});
@@ -43,6 +43,8 @@ describe('deleteFlatEntityFromFlatEntityAndRelatedEntityMapsThroughMutationOrThr
workspaceId,
calendarViewIds: [viewId],
calendarViewUniversalIdentifiers: [viewUniversalIdentifier],
calendarEndViewIds: [viewId],
calendarEndViewUniversalIdentifiers: [viewUniversalIdentifier],
mainGroupByFieldMetadataViewIds: [],
applicationId,
});
@@ -58,6 +60,8 @@ describe('deleteFlatEntityFromFlatEntityAndRelatedEntityMapsThroughMutationOrThr
workspaceId,
calendarFieldMetadataId: mockFieldMetadata.id,
calendarFieldMetadataUniversalIdentifier: fieldUniversalIdentifier,
calendarEndFieldMetadataId: mockFieldMetadata.id,
calendarEndFieldMetadataUniversalIdentifier: fieldUniversalIdentifier,
createdAt: new Date('2024-01-01').toISOString(),
updatedAt: new Date('2024-01-01').toISOString(),
icon: 'icon',
@@ -114,6 +118,8 @@ describe('deleteFlatEntityFromFlatEntityAndRelatedEntityMapsThroughMutationOrThr
).toMatchObject<Partial<FlatFieldMetadata>>({
calendarViewIds: [],
calendarViewUniversalIdentifiers: [],
calendarEndViewIds: [],
calendarEndViewUniversalIdentifiers: [],
});
});
});