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
@@ -161,6 +161,7 @@ const buildLegacyCalendarEventRecordingPreferenceFieldMetadata = ({
fieldPermissionIds: [],
kanbanAggregateOperationViewIds: [],
calendarViewIds: [],
calendarEndViewIds: [],
mainGroupByFieldMetadataViewIds: [],
createdAt: now,
updatedAt: now,
@@ -175,6 +176,7 @@ const buildLegacyCalendarEventRecordingPreferenceFieldMetadata = ({
fieldPermissionUniversalIdentifiers: [],
kanbanAggregateOperationViewUniversalIdentifiers: [],
calendarViewUniversalIdentifiers: [],
calendarEndViewUniversalIdentifiers: [],
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
viewSortIds: [],
viewSortUniversalIdentifiers: [],
@@ -0,0 +1,33 @@
import { QueryRunner } from 'typeorm';
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
@RegisteredInstanceCommand('2.22.0', 1783956795000)
export class AddCalendarEndFieldMetadataIdToViewFastInstanceCommand
implements FastInstanceCommand
{
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'ALTER TABLE "core"."view" ADD COLUMN IF NOT EXISTS "calendarEndFieldMetadataId" uuid',
);
await queryRunner.query(
'CREATE INDEX IF NOT EXISTS "IDX_VIEW_CALENDAR_END_FIELD_METADATA" ON "core"."view" ("calendarEndFieldMetadataId") ',
);
await queryRunner.query(
'DO $$ BEGIN ALTER TABLE "core"."view" ADD CONSTRAINT "FK_e1d69dd7402cd7df3b03ce11311" FOREIGN KEY ("calendarEndFieldMetadataId") REFERENCES "core"."fieldMetadata"("id") ON DELETE SET NULL ON UPDATE NO ACTION; EXCEPTION WHEN duplicate_object THEN NULL; END $$',
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'ALTER TABLE "core"."view" DROP CONSTRAINT IF EXISTS "FK_e1d69dd7402cd7df3b03ce11311"',
);
await queryRunner.query(
'DROP INDEX IF EXISTS "core"."IDX_VIEW_CALENDAR_END_FIELD_METADATA"',
);
await queryRunner.query(
'ALTER TABLE "core"."view" DROP COLUMN IF EXISTS "calendarEndFieldMetadataId"',
);
}
}
@@ -0,0 +1,49 @@
import { type QueryRunner } from 'typeorm';
import { AddCalendarEndFieldMetadataIdToViewFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-22/2-22-instance-command-fast-1783956795000-add-calendar-end-field-metadata-id-to-view';
import { ADD_CALENDAR_END_FIELD_METADATA_ID_TO_VIEW_UPGRADE_COMMAND_NAME } from 'src/database/commands/upgrade-version-command/2-22/add-calendar-end-field-metadata-id-to-view-upgrade-command-name.constant';
import { getRegisteredInstanceCommandMetadata } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
describe('AddCalendarEndFieldMetadataIdToViewFastInstanceCommand', () => {
const command =
new AddCalendarEndFieldMetadataIdToViewFastInstanceCommand();
it('is registered against the current version with a matching name', () => {
const metadata = getRegisteredInstanceCommandMetadata(
AddCalendarEndFieldMetadataIdToViewFastInstanceCommand,
);
expect(metadata).toEqual({
timestamp: 1783956795000,
type: 'fast',
version: '2.22.0',
});
expect(
`${metadata?.version}_${AddCalendarEndFieldMetadataIdToViewFastInstanceCommand.name}_${metadata?.timestamp}`,
).toBe(ADD_CALENDAR_END_FIELD_METADATA_ID_TO_VIEW_UPGRADE_COMMAND_NAME);
});
it('adds the column, index and foreign key idempotently', async () => {
const query = jest.fn().mockResolvedValue(undefined);
await command.up({ query } as unknown as QueryRunner);
expect(query.mock.calls.map((call) => call[0] as string)).toEqual([
'ALTER TABLE "core"."view" ADD COLUMN IF NOT EXISTS "calendarEndFieldMetadataId" uuid',
'CREATE INDEX IF NOT EXISTS "IDX_VIEW_CALENDAR_END_FIELD_METADATA" ON "core"."view" ("calendarEndFieldMetadataId") ',
'DO $$ BEGIN ALTER TABLE "core"."view" ADD CONSTRAINT "FK_e1d69dd7402cd7df3b03ce11311" FOREIGN KEY ("calendarEndFieldMetadataId") REFERENCES "core"."fieldMetadata"("id") ON DELETE SET NULL ON UPDATE NO ACTION; EXCEPTION WHEN duplicate_object THEN NULL; END $$',
]);
});
it('removes the foreign key, index and column idempotently', async () => {
const query = jest.fn().mockResolvedValue(undefined);
await command.down({ query } as unknown as QueryRunner);
expect(query.mock.calls.map((call) => call[0] as string)).toEqual([
'ALTER TABLE "core"."view" DROP CONSTRAINT IF EXISTS "FK_e1d69dd7402cd7df3b03ce11311"',
'DROP INDEX IF EXISTS "core"."IDX_VIEW_CALENDAR_END_FIELD_METADATA"',
'ALTER TABLE "core"."view" DROP COLUMN IF EXISTS "calendarEndFieldMetadataId"',
]);
});
});
@@ -0,0 +1,2 @@
export const ADD_CALENDAR_END_FIELD_METADATA_ID_TO_VIEW_UPGRADE_COMMAND_NAME =
'2.22.0_AddCalendarEndFieldMetadataIdToViewFastInstanceCommand_1783956795000';
@@ -110,6 +110,7 @@ import { BackfillGalleryImagesOnApplicationRegistrationSlowInstanceCommand } fro
import { AddWorkflowVersionSyncableColumnsFastInstanceCommand } from './2-20/2-20-instance-command-fast-1783603454480-add-workflow-version-syncable-columns';
import { BackfillWorkspaceDatabaseSchemaSlowInstanceCommand } from './2-21/2-21-instance-command-slow-1783934147089-backfill-workspace-database-schema';
import { AddLogoFileIdToApplicationRegistrationFastInstanceCommand } from './2-21/2-21-instance-command-fast-1783945979243-add-logo-file-id-to-application-registration';
import { AddCalendarEndFieldMetadataIdToViewFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-22/2-22-instance-command-fast-1783956795000-add-calendar-end-field-metadata-id-to-view';
export const INSTANCE_COMMANDS = [
AddViewFieldGroupIdIndexOnViewFieldFastInstanceCommand,
@@ -222,4 +223,5 @@ export const INSTANCE_COMMANDS = [
AddWorkflowVersionSyncableColumnsFastInstanceCommand,
BackfillWorkspaceDatabaseSchemaSlowInstanceCommand,
AddLogoFileIdToApplicationRegistrationFastInstanceCommand,
AddCalendarEndFieldMetadataIdToViewFastInstanceCommand,
];