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:
+1
@@ -343,6 +343,7 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma
|
||||
"anyFieldFilterValue",
|
||||
"calendarLayout",
|
||||
"calendarFieldMetadataUniversalIdentifier",
|
||||
"calendarEndFieldMetadataUniversalIdentifier",
|
||||
"visibility",
|
||||
"mainGroupByFieldMetadataUniversalIdentifier",
|
||||
"shouldHideEmptyGroups",
|
||||
|
||||
+1
@@ -69,6 +69,7 @@ exports[`registry-derived override property maps derives the overridable propert
|
||||
"anyFieldFilterValue",
|
||||
"calendarLayout",
|
||||
"calendarFieldMetadataId",
|
||||
"calendarEndFieldMetadataId",
|
||||
"visibility",
|
||||
"mainGroupByFieldMetadataId",
|
||||
"shouldHideEmptyGroups",
|
||||
|
||||
+6
@@ -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,
|
||||
|
||||
+3
@@ -98,6 +98,9 @@ export const ALL_MANY_TO_ONE_METADATA_FOREIGN_KEY = {
|
||||
calendarFieldMetadata: {
|
||||
foreignKey: 'calendarFieldMetadataId',
|
||||
},
|
||||
calendarEndFieldMetadata: {
|
||||
foreignKey: 'calendarEndFieldMetadataId',
|
||||
},
|
||||
kanbanAggregateOperationFieldMetadata: {
|
||||
foreignKey: 'kanbanAggregateOperationFieldMetadataId',
|
||||
},
|
||||
|
||||
+7
@@ -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',
|
||||
|
||||
+6
@@ -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',
|
||||
|
||||
+3
-1
@@ -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'
|
||||
|
||||
+2
-1
@@ -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'
|
||||
|
||||
+4
@@ -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],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+6
@@ -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: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user