mirror of https://github.com/Squidex/squidex.git
Browse Source
* Calendar view for contents. * More UI fixes. * Another UI fix. * Localization. * More UI fixes. * Fix ID query. * Title for schema name. * Show only references that can be read. * Test fixed * Reference dropdown fix and asset fix.pull/744/head
committed by
GitHub
54 changed files with 696 additions and 141 deletions
@ -0,0 +1,51 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Microsoft.AspNetCore.Mvc; |
||||
|
using NodaTime; |
||||
|
using Squidex.Domain.Apps.Entities; |
||||
|
using Squidex.Infrastructure.Translations; |
||||
|
using Squidex.Infrastructure.Validation; |
||||
|
|
||||
|
namespace Squidex.Areas.Api.Controllers.Contents.Models |
||||
|
{ |
||||
|
public sealed class AllContentsByGetDto |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The list of ids to query.
|
||||
|
/// </summary>
|
||||
|
[FromQuery(Name = "ids")] |
||||
|
public string? Ids { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The start of the schedule.
|
||||
|
/// </summary>
|
||||
|
[FromQuery] |
||||
|
public Instant? ScheduledFrom { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The end of the schedule.
|
||||
|
/// </summary>
|
||||
|
[FromQuery] |
||||
|
public Instant? ScheduledTo { get; set; } |
||||
|
|
||||
|
public Q ToQuery() |
||||
|
{ |
||||
|
if (!string.IsNullOrWhiteSpace(Ids)) |
||||
|
{ |
||||
|
return Q.Empty.WithIds(Ids); |
||||
|
} |
||||
|
|
||||
|
if (ScheduledFrom != null && ScheduledTo != null) |
||||
|
{ |
||||
|
return Q.Empty.WithSchedule(ScheduledFrom.Value, ScheduledTo.Value); |
||||
|
} |
||||
|
|
||||
|
throw new ValidationException(T.Get("contents.invalidAllQuery")); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,48 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using NodaTime; |
||||
|
using Squidex.Domain.Apps.Entities; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Translations; |
||||
|
using Squidex.Infrastructure.Validation; |
||||
|
|
||||
|
namespace Squidex.Areas.Api.Controllers.Contents.Models |
||||
|
{ |
||||
|
public sealed class AllContentsByPostDto |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The list of ids to query.
|
||||
|
/// </summary>
|
||||
|
public DomainId[]? Ids { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The start of the schedule.
|
||||
|
/// </summary>
|
||||
|
public Instant? ScheduledFrom { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The end of the schedule.
|
||||
|
/// </summary>
|
||||
|
public Instant? ScheduledTo { get; set; } |
||||
|
|
||||
|
public Q ToQuery() |
||||
|
{ |
||||
|
if (Ids?.Length > 0) |
||||
|
{ |
||||
|
return Q.Empty.WithIds(Ids); |
||||
|
} |
||||
|
|
||||
|
if (ScheduledFrom != null && ScheduledTo != null) |
||||
|
{ |
||||
|
return Q.Empty.WithSchedule(ScheduledFrom.Value, ScheduledTo.Value); |
||||
|
} |
||||
|
|
||||
|
throw new ValidationException(T.Get("contents.invalidAllQuery")); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,22 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System.Collections.Generic; |
|
||||
using Squidex.Infrastructure; |
|
||||
using Squidex.Infrastructure.Validation; |
|
||||
|
|
||||
namespace Squidex.Areas.Api.Controllers.Contents.Models |
|
||||
{ |
|
||||
public sealed class ContentsIdsQueryDto |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// The list of ids to query.
|
|
||||
/// </summary>
|
|
||||
[LocalizedRequired] |
|
||||
public List<DomainId> Ids { get; set; } |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,114 @@ |
|||||
|
<sqx-title message="i18n:contents.calendar"></sqx-title> |
||||
|
|
||||
|
<sqx-layout layout="main" titleText="i18n:contents.calendar" [hideSidebar]="true"> |
||||
|
<ng-container menu> |
||||
|
{{title}} |
||||
|
|
||||
|
<select class="form-select ms-4" [ngModel]="view" (ngModelChange)="changeView($event)" [disabled]="isLoading"> |
||||
|
<option ngValue="day">{{ 'common.daily' | sqxTranslate }}</option> |
||||
|
<option ngValue="week">{{ 'common.weekly' | sqxTranslate }}</option> |
||||
|
<option ngValue="month">{{ 'common.monthly' | sqxTranslate }}</option> |
||||
|
</select> |
||||
|
|
||||
|
<button type="button" class="btn btn-text-secondary btn-navigate ms-2" (click)="goPrev()" [disabled]="isLoading"> |
||||
|
<i class="icon-caret-left"></i> |
||||
|
</button> |
||||
|
<button type="button" class="btn btn-text-secondary btn-navigate ms-2" (click)="goNext()" [disabled]="isLoading"> |
||||
|
<i class="icon-caret-right"></i> |
||||
|
</button> |
||||
|
</ng-container> |
||||
|
|
||||
|
<ng-container content> |
||||
|
<div class="calendar" #calendarContainer></div> |
||||
|
</ng-container> |
||||
|
</sqx-layout> |
||||
|
|
||||
|
<ng-container *sqxModal="contentDialog"> |
||||
|
<sqx-modal-dialog (close)="contentDialog.hide()"> |
||||
|
<ng-container title> |
||||
|
{{ 'common.content' | sqxTranslate }} |
||||
|
</ng-container> |
||||
|
|
||||
|
<ng-container content> |
||||
|
<div *ngIf="content && content.scheduleJob"> |
||||
|
<div class="form-group row"> |
||||
|
<label class="col-4 col-form-label">{{ 'common.id' | sqxTranslate }}</label> |
||||
|
|
||||
|
<div class="col-8"> |
||||
|
<div class="input-group"> |
||||
|
<input readonly class="form-control" name="id" id="id" value="{{content.id}}" #inputId> |
||||
|
|
||||
|
<button type="button" class="btn btn-outline-secondary" [sqxCopy]="inputId"> |
||||
|
<i class="icon-copy"></i> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-group form-group-aligned row"> |
||||
|
<label class="col-4 col-form-label">{{ 'common.content' | sqxTranslate }}</label> |
||||
|
|
||||
|
<div class="col-8"> |
||||
|
<a class="truncate" [routerLink]="['../', content.schemaName, content.id]"> |
||||
|
{{createContentName(content)}} |
||||
|
</a> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-group form-group-aligned row"> |
||||
|
<label class="col-4 col-form-label">{{ 'common.schema' | sqxTranslate }}</label> |
||||
|
|
||||
|
<div class="col-8"> |
||||
|
<a class="truncate" [routerLink]="['../', content.schemaName]"> |
||||
|
{{content.schemaDisplayName}} |
||||
|
</a> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-group form-group-aligned row"> |
||||
|
<label class="col-4 col-form-label">{{ 'common.status' | sqxTranslate }}</label> |
||||
|
|
||||
|
<div class="col-8"> |
||||
|
<sqx-content-status |
||||
|
layout="text" |
||||
|
[status]="content.status" |
||||
|
[statusColor]="content.statusColor" |
||||
|
[small]="true"> |
||||
|
</sqx-content-status> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<hr /> |
||||
|
|
||||
|
<div class="form-group form-group-aligned row"> |
||||
|
<label class="col-4 col-form-label">{{ 'contents.scheduledToLabel' | sqxTranslate }}</label> |
||||
|
|
||||
|
<div class="col-8"> |
||||
|
<sqx-content-status |
||||
|
layout="text" |
||||
|
[status]="content.scheduleJob.status" |
||||
|
[statusColor]="content.scheduleJob.color" |
||||
|
[small]="true"> |
||||
|
</sqx-content-status> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-group form-group-aligned row"> |
||||
|
<label class="col-4 col-form-label">{{ 'contents.scheduledAt' | sqxTranslate }}</label> |
||||
|
|
||||
|
<div class="col-8"> |
||||
|
{{content.scheduleJob.dueTime | sqxFullDateTime}} |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-group form-group-aligned row"> |
||||
|
<label class="col-4 col-form-label">{{ 'contents.scheduledBy' | sqxTranslate }}</label> |
||||
|
|
||||
|
<div class="col-8"> |
||||
|
<img class="user-picture" [src]="content.scheduleJob.scheduledBy | sqxUserPictureRef"> {{content.scheduleJob.scheduledBy | sqxUserNameRef}} |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</ng-container> |
||||
|
</sqx-modal-dialog> |
||||
|
</ng-container> |
||||
@ -0,0 +1,24 @@ |
|||||
|
:host ::ng-deep { |
||||
|
.tui-full-calendar-weekday-schedule-bullet { |
||||
|
top: 10px !important; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.calendar { |
||||
|
@include absolute(-1px, 0, 0, 0); |
||||
|
} |
||||
|
|
||||
|
.form-select { |
||||
|
display: inline-block; |
||||
|
white-space: normal; |
||||
|
width: auto; |
||||
|
} |
||||
|
|
||||
|
.form-group-aligned { |
||||
|
align-items: center; |
||||
|
|
||||
|
.col-form-label { |
||||
|
padding-bottom: 0; |
||||
|
padding-top: 0; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,210 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, OnDestroy, ViewChild } from '@angular/core'; |
||||
|
import { AppsState, ContentDto, ContentsService, DateTime, DialogModel, getContentValue, LanguageDto, LanguagesState, LocalizerService, ResourceLoaderService } from '@app/shared'; |
||||
|
|
||||
|
declare const tui: any; |
||||
|
|
||||
|
type ViewMode = 'day' | 'week' | 'month'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'sqx-calendar-page', |
||||
|
styleUrls: ['./calendar-page.component.scss'], |
||||
|
templateUrl: './calendar-page.component.html', |
||||
|
}) |
||||
|
export class CalendarPageComponent implements AfterViewInit, OnDestroy { |
||||
|
private calendar: any; |
||||
|
private language: LanguageDto; |
||||
|
|
||||
|
@ViewChild('calendarContainer', { static: false }) |
||||
|
public calendarContainer: ElementRef; |
||||
|
|
||||
|
public view: ViewMode = 'month'; |
||||
|
|
||||
|
public content?: ContentDto; |
||||
|
public contentDialog = new DialogModel(); |
||||
|
|
||||
|
public title: string; |
||||
|
|
||||
|
public isLoading: boolean; |
||||
|
|
||||
|
constructor( |
||||
|
private readonly appsState: AppsState, |
||||
|
private readonly changeDetector: ChangeDetectorRef, |
||||
|
private readonly contentsService: ContentsService, |
||||
|
private readonly resourceLoader: ResourceLoaderService, |
||||
|
private readonly languagesState: LanguagesState, |
||||
|
private readonly localizer: LocalizerService, |
||||
|
) { |
||||
|
} |
||||
|
|
||||
|
public ngOnDestroy() { |
||||
|
this.calendar?.destroy(); |
||||
|
} |
||||
|
|
||||
|
public ngOnInit() { |
||||
|
this.language = this.languagesState.snapshot.languages.find(x => x.language.isMaster)!.language; |
||||
|
} |
||||
|
|
||||
|
public ngAfterViewInit() { |
||||
|
Promise.all([ |
||||
|
this.resourceLoader.loadLocalStyle('dependencies/tui-calendar/tui-calendar.min.css'), |
||||
|
this.resourceLoader.loadLocalScript('dependencies/tui-calendar/tui-code-snippet.min.js'), |
||||
|
this.resourceLoader.loadLocalScript('dependencies/tui-calendar/tui-calendar.min.js'), |
||||
|
]).then(() => { |
||||
|
const Calendar = tui.Calendar; |
||||
|
|
||||
|
this.calendar = new Calendar(this.calendarContainer.nativeElement, { |
||||
|
taskView: false, |
||||
|
scheduleView: ['time'], |
||||
|
defaultView: 'month', |
||||
|
isReadOnly: true, |
||||
|
...getLocalizationSettings(), |
||||
|
}); |
||||
|
|
||||
|
this.calendar.on('clickSchedule', (event: any) => { |
||||
|
this.content = event.schedule.raw; |
||||
|
this.contentDialog.show(); |
||||
|
|
||||
|
this.changeDetector.detectChanges(); |
||||
|
}); |
||||
|
|
||||
|
this.load(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public changeView(view: ViewMode) { |
||||
|
this.view = view; |
||||
|
|
||||
|
this.calendar?.changeView(view); |
||||
|
|
||||
|
this.load(); |
||||
|
} |
||||
|
|
||||
|
public goPrev() { |
||||
|
this.calendar?.prev(); |
||||
|
|
||||
|
this.load(); |
||||
|
} |
||||
|
|
||||
|
public goNext() { |
||||
|
this.calendar?.next(); |
||||
|
|
||||
|
this.load(); |
||||
|
} |
||||
|
|
||||
|
private load() { |
||||
|
if (!this.calendar) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
const scheduledFrom = new DateTime(this.calendar.getDateRangeStart().toDate()); |
||||
|
const scheduledTo = new DateTime(this.calendar.getDateRangeEnd().toDate()); |
||||
|
|
||||
|
this.updateRange(scheduledFrom, scheduledTo); |
||||
|
|
||||
|
if (this.isLoading) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
this.isLoading = true; |
||||
|
|
||||
|
this.contentsService.getAllContents(this.appsState.appName, { |
||||
|
scheduledFrom: scheduledFrom.toISOString(), |
||||
|
scheduledTo: scheduledTo.toISOString(), |
||||
|
}).subscribe({ |
||||
|
next: contents => { |
||||
|
this.calendar.clear(); |
||||
|
this.calendar.createSchedules(contents.items.map(x => ({ |
||||
|
id: x.id, |
||||
|
borderColor: x.scheduleJob!.color, |
||||
|
color: x.scheduleJob?.color, |
||||
|
calendarId: '1', |
||||
|
category: 'time', |
||||
|
end: x.scheduleJob?.dueTime.toISOString(), |
||||
|
raw: x, |
||||
|
start: x.scheduleJob?.dueTime.toISOString(), |
||||
|
state: 'free', |
||||
|
title: `[${x.schemaDisplayName}] ${this.createContentName(x)}`, |
||||
|
}))); |
||||
|
}, |
||||
|
complete: () => { |
||||
|
this.isLoading = false; |
||||
|
}, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private updateRange(from: DateTime, to: DateTime) { |
||||
|
switch (this.view) { |
||||
|
case 'month': { |
||||
|
this.title = from.toStringFormat('LLLL yyyy'); |
||||
|
break; |
||||
|
} |
||||
|
case 'day': { |
||||
|
this.title = from.toStringFormat('PPPP'); |
||||
|
break; |
||||
|
} |
||||
|
case 'week': { |
||||
|
this.title = `${from.toStringFormat('PP')} - ${to.toStringFormat('PP')}`; |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public createContentName(content: ContentDto) { |
||||
|
const name = |
||||
|
content.referenceFields |
||||
|
.map(f => getContentValue(content, this.language, f, false)) |
||||
|
.map(v => v.formatted) |
||||
|
.filter(v => !!v) |
||||
|
.join(', ') |
||||
|
|| this.localizer.getOrKey('common.noValue'); |
||||
|
|
||||
|
return name; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
let localizedValues: any; |
||||
|
|
||||
|
function getLocalizationSettings() { |
||||
|
if (!localizedValues) { |
||||
|
localizedValues = { |
||||
|
month: { |
||||
|
daynames: [], |
||||
|
}, |
||||
|
week: { |
||||
|
daynames: [], |
||||
|
}, |
||||
|
template: { |
||||
|
timegridDisplayPrimaryTime: (time: any) => { |
||||
|
return new DateTime(new Date(2020, 1, 1, time.hour, time.minutes, 0)).toStringFormat('p'); |
||||
|
}, |
||||
|
timegridCurrentTime: (timezone: any) => { |
||||
|
const templates = []; |
||||
|
|
||||
|
if (timezone.dateDifference) { |
||||
|
templates.push(`[${timezone.dateDifferenceSign}${timezone.dateDifference}]<br>`); |
||||
|
} |
||||
|
|
||||
|
templates.push(new DateTime(timezone.hourmarker.toDate()).toStringFormat('p')); |
||||
|
|
||||
|
return templates.join(''); |
||||
|
}, |
||||
|
}, |
||||
|
}; |
||||
|
|
||||
|
for (let i = 1; i <= 7; i++) { |
||||
|
const weekDay = new DateTime(new Date(2020, 10, i, 12, 0, 0)); |
||||
|
|
||||
|
localizedValues.month.daynames.push(weekDay.toStringFormat('EEE')); |
||||
|
localizedValues.week.daynames.push(weekDay.toStringFormat('EEE')); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return localizedValues; |
||||
|
} |
||||
Loading…
Reference in new issue