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