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:
+2
@@ -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: [],
|
||||
|
||||
+33
@@ -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"',
|
||||
);
|
||||
}
|
||||
}
|
||||
+49
@@ -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"',
|
||||
]);
|
||||
});
|
||||
});
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export const ADD_CALENDAR_END_FIELD_METADATA_ID_TO_VIEW_UPGRADE_COMMAND_NAME =
|
||||
'2.22.0_AddCalendarEndFieldMetadataIdToViewFastInstanceCommand_1783956795000';
|
||||
+2
@@ -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,
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user