Skip to content

Commit 1db5547

Browse files
authored
Calendar view (#1174)
Signed-off-by: Andrey Sobolev <[email protected]>
1 parent 4577878 commit 1db5547

File tree

19 files changed

+654
-15
lines changed

19 files changed

+654
-15
lines changed

Diff for: models/calendar/src/index.ts

+18
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import core, { TAttachedDoc } from '@anticrm/model-core'
2525
import { TSpaceWithStates } from '@anticrm/model-task'
2626
import workbench from '@anticrm/model-workbench'
2727
import calendar from './plugin'
28+
import view from '@anticrm/model-view'
2829

2930
export * from '@anticrm/calendar'
3031

@@ -35,6 +36,7 @@ export const DOMAIN_CALENDAR = 'calendar' as Domain
3536
export class TCalendar extends TSpaceWithStates implements Calendar {}
3637

3738
@Model(calendar.class.Event, core.class.AttachedDoc, DOMAIN_CALENDAR)
39+
@UX(calendar.string.Event, calendar.icon.Calendar)
3840
export class TEvent extends TAttachedDoc implements Event {
3941
@Prop(TypeString(), calendar.string.Title)
4042
@Index(IndexKind.FullText)
@@ -85,6 +87,22 @@ export function createModel (builder: Builder): void {
8587
]
8688
}
8789
}, calendar.app.Calendar)
90+
91+
builder.createDoc(
92+
view.class.ViewletDescriptor,
93+
core.space.Model,
94+
{
95+
label: calendar.string.Calendar,
96+
icon: calendar.icon.Calendar,
97+
component: calendar.component.CalendarView
98+
},
99+
calendar.viewlet.Calendar
100+
)
101+
102+
// Use generic child presenter
103+
builder.mixin(calendar.class.Event, core.class.Class, view.mixin.AttributePresenter, {
104+
presenter: view.component.ObjectPresenter
105+
})
88106
}
89107

90108
export default calendar

Diff for: models/calendar/src/plugin.ts

+8-3
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,22 @@
1515

1616
import { calendarId } from '@anticrm/calendar'
1717
import calendar from '@anticrm/calendar-resources/src/plugin'
18+
import { Ref } from '@anticrm/core'
1819
import type { IntlString } from '@anticrm/platform'
1920
import { mergeIds } from '@anticrm/platform'
2021
import { AnyComponent } from '@anticrm/ui'
22+
import { ViewletDescriptor } from '@anticrm/view'
2123

2224
export default mergeIds(calendarId, calendar, {
2325
component: {
24-
CreateCalendar: '' as AnyComponent
26+
CreateCalendar: '' as AnyComponent,
27+
CalendarView: '' as AnyComponent
2528
},
2629
string: {
27-
ApplicationLabelCalendar: '' as IntlString
30+
ApplicationLabelCalendar: '' as IntlString,
31+
Event: '' as IntlString
2832
},
29-
space: {
33+
viewlet: {
34+
Calendar: '' as Ref<ViewletDescriptor>
3035
}
3136
})

Diff for: models/recruit/src/review.ts

+14
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,20 @@ export function createReviewModel (builder: Builder): void {
8181
archived: false
8282
}
8383
})
84+
85+
builder.createDoc(view.class.Viewlet, core.space.Model, {
86+
attachTo: recruit.class.Review,
87+
descriptor: calendar.viewlet.Calendar,
88+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
89+
options: {
90+
lookup: {
91+
attachedTo: recruit.mixin.Candidate,
92+
participants: contact.class.Employee,
93+
company: contact.class.Organization
94+
}
95+
} as FindOptions<Doc>,
96+
config: []
97+
})
8498
}
8599

86100
function createTableViewlet (builder: Builder): void {

Diff for: packages/ui/lang/en.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
"NotSelected": "Not selected",
1717
"Today": "Today",
1818
"English": "English",
19-
"Russian": "Russian"
19+
"Russian": "Russian",
20+
"CalendarLeft": "<",
21+
"CalendarRight": ">"
2022
}
2123
}

Diff for: packages/ui/lang/ru.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
"NotSelected": "Не выбрано",
1717
"Today": "Сегодня",
1818
"English": "Английский",
19-
"Russian": "Русский"
19+
"Russian": "Русский",
20+
"CalendarLeft": "<",
21+
"CalendarRight": ">"
2022
}
2123
}

Diff for: packages/ui/src/components/Button.svelte

+4-2
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
import Icon from './Icon.svelte'
2121
import { onMount } from 'svelte'
2222
23-
export let label: IntlString
23+
export let label: IntlString | undefined = undefined
2424
export let primary: boolean = false
2525
export let size: 'small' | 'medium' = 'medium'
2626
export let icon: Asset | AnySvelteComponent | undefined = undefined
@@ -49,7 +49,9 @@
4949
{#if loading}
5050
<Spinner />
5151
{:else}
52-
<Label {label} />
52+
{#if label}
53+
<Label {label} />
54+
{/if}
5355
{/if}
5456
</button>
5557

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
<!--
2+
// Copyright © 2022 Hardcore Engineering Inc.
3+
//
4+
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License. You may
6+
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
//
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
-->
15+
<script type="ts">
16+
import { areDatesEqual, day, firstDay, getWeekDayName, isWeekend, weekday } from './internal/DateUtils'
17+
18+
export let mondayStart = true
19+
export let weekFormat: 'narrow' | 'short' | 'long' | undefined = 'short'
20+
export let cellHeight: string | undefined = undefined
21+
export let value: Date = new Date()
22+
export let currentDate: Date = new Date()
23+
export let displayedWeeksCount = 6
24+
25+
$: firstDayOfCurrentMonth = firstDay(currentDate, mondayStart)
26+
27+
function onSelect (date: Date) {
28+
value = date
29+
}
30+
31+
const todayDate = new Date()
32+
</script>
33+
34+
<div class="month-calendar">
35+
<div class="days-of-week-header">
36+
{#each [...Array(7).keys()] as dayOfWeek}
37+
<div class="day-name">{getWeekDayName(day(firstDayOfCurrentMonth, dayOfWeek), weekFormat)}</div>
38+
{/each}
39+
</div>
40+
<div class="days-of-month">
41+
{#each [...Array(displayedWeeksCount).keys()] as weekIndex}
42+
{#each [...Array(7).keys()] as dayOfWeek}
43+
<div style={`grid-column-start: ${dayOfWeek + 1}; grid-row-start: ${weekIndex + 1}`}>
44+
<div style={`display: flex; width: 100%; height: ${cellHeight ? `${cellHeight};` : '100%;'}`}>
45+
<div
46+
class="cell flex-center"
47+
class:weekend={isWeekend(weekday(firstDayOfCurrentMonth, weekIndex, dayOfWeek))}
48+
class:today={areDatesEqual(todayDate, weekday(firstDayOfCurrentMonth, weekIndex, dayOfWeek))}
49+
class:selected={weekday(firstDayOfCurrentMonth, weekIndex, dayOfWeek).getMonth() ===
50+
currentDate.getMonth() && areDatesEqual(value, weekday(firstDayOfCurrentMonth, weekIndex, dayOfWeek))}
51+
class:wrongMonth={weekday(firstDayOfCurrentMonth, weekIndex, dayOfWeek).getMonth() !==
52+
currentDate.getMonth()}
53+
on:click={() => onSelect(weekday(firstDayOfCurrentMonth, weekIndex, dayOfWeek))}
54+
>
55+
{#if !$$slots.cell || weekday(firstDayOfCurrentMonth, weekIndex, dayOfWeek).getMonth() !== currentDate.getMonth()}
56+
{weekday(firstDayOfCurrentMonth, weekIndex, dayOfWeek).getDate()}
57+
{:else}
58+
<slot name="cell" date={weekday(firstDayOfCurrentMonth, weekIndex, dayOfWeek)} />
59+
{/if}
60+
</div>
61+
</div>
62+
</div>
63+
{/each}
64+
{/each}
65+
</div>
66+
</div>
67+
68+
<style lang="scss">
69+
.day-name,
70+
.selected-month-controller {
71+
display: flex;
72+
justify-content: center;
73+
}
74+
.days-of-week-header,
75+
.days-of-month {
76+
display: grid;
77+
grid-template-columns: repeat(7, 1fr);
78+
}
79+
.weekend {
80+
background-color: var(--theme-bg-accent-color);
81+
}
82+
.today {
83+
color: #a66600;
84+
}
85+
.selected {
86+
border-radius: 3px;
87+
background-color: var(--primary-button-enabled);
88+
border-color: var(--primary-button-focused-border);
89+
color: var(--primary-button-color);
90+
}
91+
.cell {
92+
height: 100%;
93+
width: 100%;
94+
border-radius: 0.5rem;
95+
border: 1px solid transparent;
96+
}
97+
.cell:hover:not(.wrongMonth) {
98+
border: 1px solid var(--primary-button-focused-border);
99+
background-color: var(--primary-button-enabled);
100+
color: var(--primary-button-color);
101+
}
102+
.wrongMonth {
103+
color: var(--grayscale-grey-03);
104+
}
105+
.month-name {
106+
font-size: 14px;
107+
font-weight: bold;
108+
margin: 0 5px;
109+
color: var(--theme-content-dark-color);
110+
}
111+
</style>
+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<!--
2+
// Copyright © 2022 Hardcore Engineering Inc.
3+
//
4+
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License. You may
6+
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
//
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
-->
15+
<script type="ts">
16+
import MonthCalendar from './MonthCalendar.svelte'
17+
18+
/**
19+
* If passed, calendars will use monday as first day
20+
*/
21+
export let mondayStart = true
22+
export let value: Date = new Date()
23+
export let currentDate: Date = new Date()
24+
export let cellHeight: string | undefined = undefined
25+
export let minWidth = '18rem'
26+
27+
function getMonthName (date: Date): string {
28+
const locale = new Intl.NumberFormat().resolvedOptions().locale
29+
return new Intl.DateTimeFormat(locale, { month: 'long' }).format(date)
30+
}
31+
function month (date: Date, m: number): Date {
32+
date = new Date(date)
33+
date.setMonth(m)
34+
return date
35+
}
36+
</script>
37+
38+
<div class="year-erp-calendar">
39+
{#each [...Array(12).keys()] as m}
40+
<div class="antiComponentBox mt-2 mb-2 ml-2 mr-2 flex-grow" style={`min-width: ${minWidth};`}>
41+
{getMonthName(month(value, m))}
42+
<MonthCalendar {cellHeight} weekFormat="narrow" bind:value currentDate={month(currentDate, m)} {mondayStart}>
43+
<!----> eslint-disable-next-line no-undef -->
44+
<svelte:fragment slot="cell" let:date={date}>
45+
<slot name="cell" date={date} />
46+
</svelte:fragment>
47+
</MonthCalendar>
48+
</div>
49+
{/each}
50+
</div>
51+
52+
<style lang="scss">
53+
.year-erp-calendar {
54+
display: grid;
55+
grid-template-columns: repeat(4, 1fr);
56+
border-collapse: collapse;
57+
.row {
58+
display: table-row;
59+
}
60+
.th {
61+
display: table-cell;
62+
}
63+
.calendar {
64+
display: table-cell;
65+
padding: 0.3em;
66+
}
67+
}
68+
</style>
+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// Copyright © 2022 Hardcore Engineering Inc.
2+
//
3+
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License. You may
5+
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
6+
//
7+
// Unless required by applicable law or agreed to in writing, software
8+
// distributed under the License is distributed on an "AS IS" BASIS,
9+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
//
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
15+
const DAYS_IN_WEEK = 7
16+
const MILLISECONDS_IN_DAY = 86400000
17+
18+
export function firstDay (date: Date, mondayStart: boolean): Date {
19+
const firstDayOfMonth = new Date(date)
20+
firstDayOfMonth.setDate(1) // First day of month
21+
const result = new Date(firstDayOfMonth)
22+
result.setDate(
23+
result.getDate() - result.getDay() + (mondayStart ? 1 : 0)
24+
)
25+
// Check if we need add one more week
26+
if (result.getTime() > firstDayOfMonth.getTime()) {
27+
result.setDate(result.getDate() - DAYS_IN_WEEK)
28+
}
29+
result.setHours(0)
30+
result.setMinutes(0)
31+
result.setSeconds(0)
32+
result.setMilliseconds(0)
33+
return result
34+
}
35+
36+
export function getWeekDayName (weekDay: Date, weekFormat: 'narrow' | 'short' | 'long' | undefined = 'short'): string {
37+
const locale = new Intl.NumberFormat().resolvedOptions().locale
38+
return new Intl.DateTimeFormat(locale, {
39+
weekday: weekFormat
40+
}).format(weekDay)
41+
}
42+
43+
export function day (firstDay: Date, offset: number): Date {
44+
return new Date(firstDay.getTime() + offset * MILLISECONDS_IN_DAY)
45+
}
46+
47+
export function weekday (firstDay: Date, w: number, d: number): Date {
48+
return day(firstDay, w * DAYS_IN_WEEK + d)
49+
}
50+
51+
export function areDatesEqual (firstDate: Date | undefined, secondDate: Date | undefined): boolean {
52+
if (firstDate === undefined || secondDate === undefined) {
53+
return false
54+
}
55+
return (
56+
firstDate.getFullYear() === secondDate.getFullYear() &&
57+
firstDate.getMonth() === secondDate.getMonth() &&
58+
firstDate.getDate() === secondDate.getDate()
59+
)
60+
}
61+
62+
export function isWeekend (date: Date): boolean {
63+
return date.getDay() === 0 || date.getDay() === 6
64+
}
65+
66+
export function getMonthName (date: Date): string {
67+
const locale = new Intl.NumberFormat().resolvedOptions().locale
68+
return new Intl.DateTimeFormat(locale, { month: 'long' }).format(date)
69+
}

Diff for: packages/ui/src/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,6 @@ addStringsLoader(uiId, async (lang: string) => {
116116

117117
export { default } from './plugin'
118118
export * from './colors'
119+
120+
export { default as MonthCalendar } from './components/calendar/MonthCalendar.svelte'
121+
export { default as YearCalendar } from './components/calendar/YearCalendar.svelte'

0 commit comments

Comments
 (0)