From 3c9c67d726215b448b44947e1e204f3382e1c5df Mon Sep 17 00:00:00 2001 From: Alexander Skvortsov Date: Sun, 21 Nov 2021 19:11:18 -0500 Subject: [PATCH] Clean up model nullability --- framework/core/js/src/common/Model.ts | 10 ++++- .../core/js/src/common/helpers/avatar.tsx | 4 +- .../core/js/src/common/models/Discussion.ts | 36 +++++++++--------- framework/core/js/src/common/models/Group.ts | 4 +- .../core/js/src/common/models/Notification.ts | 6 +-- framework/core/js/src/common/models/Post.ts | 32 +++++++++------- framework/core/js/src/common/models/User.ts | 37 ++++++++++--------- .../components/DiscussionsSearchSource.tsx | 2 +- 8 files changed, 72 insertions(+), 59 deletions(-) diff --git a/framework/core/js/src/common/Model.ts b/framework/core/js/src/common/Model.ts index b38dd595a..9195c3153 100644 --- a/framework/core/js/src/common/Model.ts +++ b/framework/core/js/src/common/Model.ts @@ -310,6 +310,8 @@ export default abstract class Model { * relationship exists; undefined if the relationship exists but the model * has not been loaded; or the model if it has been loaded. */ + static hasOne(name: string): () => M | false; + static hasOne(name: string): () => M | null | false; static hasOne(name: string): () => M | false { return function (this: Model) { if (this.data.relationships) { @@ -358,8 +360,12 @@ export default abstract class Model { /** * Transform the given value into a Date object. */ - static transformDate(value: string | null): Date | null { - return value ? new Date(value) : null; + static transformDate(value: string): Date; + static transformDate(value: string | null): Date | null; + static transformDate(value: string | undefined): Date | undefined; + static transformDate(value: string | null | undefined): Date | null | undefined; + static transformDate(value: string | null | undefined): Date | null | undefined { + return value != null ? new Date(value) : value; } /** diff --git a/framework/core/js/src/common/helpers/avatar.tsx b/framework/core/js/src/common/helpers/avatar.tsx index 4de36fc14..fb60bcd4d 100644 --- a/framework/core/js/src/common/helpers/avatar.tsx +++ b/framework/core/js/src/common/helpers/avatar.tsx @@ -24,8 +24,8 @@ export default function avatar(user: User, attrs: ComponentAttrs = {}): Mithril. // uploaded image, or the first letter of their username if they haven't // uploaded one. if (user) { - const username: string = user.displayName() || '?'; - const avatarUrl: string = user.avatarUrl(); + const username = user.displayName() || '?'; + const avatarUrl = user.avatarUrl(); if (hasTitle) attrs.title = attrs.title || username; diff --git a/framework/core/js/src/common/models/Discussion.ts b/framework/core/js/src/common/models/Discussion.ts index 1fbf59b9d..e6a95e25b 100644 --- a/framework/core/js/src/common/models/Discussion.ts +++ b/framework/core/js/src/common/models/Discussion.ts @@ -16,46 +16,46 @@ export default class Discussion extends Model { } createdAt() { - return Model.attribute('createdAt', Model.transformDate).call(this); + return Model.attribute('createdAt', Model.transformDate).call(this); } user() { - return Model.hasOne('user').call(this); + return Model.hasOne('user').call(this); } firstPost() { - return Model.hasOne('firstPost').call(this); + return Model.hasOne('firstPost').call(this); } lastPostedAt() { - return Model.attribute('lastPostedAt', Model.transformDate).call(this); + return Model.attribute('lastPostedAt', Model.transformDate).call(this); } lastPostedUser() { - return Model.hasOne('lastPostedUser').call(this); + return Model.hasOne('lastPostedUser').call(this); } lastPost() { - return Model.hasOne('lastPost').call(this); + return Model.hasOne('lastPost').call(this); } lastPostNumber() { - return Model.attribute('lastPostNumber').call(this); + return Model.attribute('lastPostNumber').call(this); } commentCount() { - return Model.attribute('commentCount').call(this); + return Model.attribute('commentCount').call(this); } replyCount() { - return computed('commentCount', (commentCount) => Math.max(0, (commentCount as number) - 1)).call(this); + return computed('commentCount', (commentCount) => Math.max(0, (commentCount as number ?? 0) - 1)).call(this); } posts() { return Model.hasMany('posts').call(this); } mostRelevantPost() { - return Model.hasOne('mostRelevantPost').call(this); + return Model.hasOne('mostRelevantPost').call(this); } lastReadAt() { - return Model.attribute('lastReadAt', Model.transformDate).call(this); + return Model.attribute('lastReadAt', Model.transformDate).call(this); } lastReadPostNumber() { - return Model.attribute('lastReadPostNumber').call(this); + return Model.attribute('lastReadPostNumber').call(this); } isUnread() { return computed('unreadCount', (unreadCount) => !!unreadCount).call(this); @@ -65,26 +65,26 @@ export default class Discussion extends Model { } hiddenAt() { - return Model.attribute('hiddenAt', Model.transformDate).call(this); + return Model.attribute('hiddenAt', Model.transformDate).call(this); } hiddenUser() { - return Model.hasOne('hiddenUser').call(this); + return Model.hasOne('hiddenUser').call(this); } isHidden() { return computed('hiddenAt', (hiddenAt) => !!hiddenAt).call(this); } canReply() { - return Model.attribute('canReply').call(this); + return Model.attribute('canReply').call(this); } canRename() { - return Model.attribute('canRename').call(this); + return Model.attribute('canRename').call(this); } canHide() { - return Model.attribute('canHide').call(this); + return Model.attribute('canHide').call(this); } canDelete() { - return Model.attribute('canDelete').call(this); + return Model.attribute('canDelete').call(this); } /** diff --git a/framework/core/js/src/common/models/Group.ts b/framework/core/js/src/common/models/Group.ts index 71cc420f2..1100c572f 100644 --- a/framework/core/js/src/common/models/Group.ts +++ b/framework/core/js/src/common/models/Group.ts @@ -13,10 +13,10 @@ export default class Group extends Model { } color() { - return Model.attribute('color').call(this); + return Model.attribute('color').call(this); } icon() { - return Model.attribute('icon').call(this); + return Model.attribute('icon').call(this); } isHidden() { diff --git a/framework/core/js/src/common/models/Notification.ts b/framework/core/js/src/common/models/Notification.ts index ef763d1bc..3a2b714c9 100644 --- a/framework/core/js/src/common/models/Notification.ts +++ b/framework/core/js/src/common/models/Notification.ts @@ -9,7 +9,7 @@ export default class Notification extends Model { return Model.attribute('content').call(this); } createdAt() { - return Model.attribute('createdAt', Model.transformDate).call(this); + return Model.attribute('createdAt', Model.transformDate).call(this); } isRead() { @@ -20,9 +20,9 @@ export default class Notification extends Model { return Model.hasOne('user').call(this); } fromUser() { - return Model.hasOne('fromUser').call(this); + return Model.hasOne('fromUser').call(this); } subject() { - return Model.hasOne('subject').call(this); + return Model.hasOne('subject').call(this); } } diff --git a/framework/core/js/src/common/models/Post.ts b/framework/core/js/src/common/models/Post.ts index f9f70981c..a487a22db 100644 --- a/framework/core/js/src/common/models/Post.ts +++ b/framework/core/js/src/common/models/Post.ts @@ -13,55 +13,61 @@ export default class Post extends Model { } createdAt() { - return Model.attribute('createdAt', Model.transformDate).call(this); + return Model.attribute('createdAt', Model.transformDate).call(this); } user() { return Model.hasOne('user').call(this); } contentType() { - return Model.attribute('contentType').call(this); + return Model.attribute('contentType').call(this); } content() { - return Model.attribute('content').call(this); + return Model.attribute('content').call(this); } contentHtml() { - return Model.attribute('contentHtml').call(this); + return Model.attribute('contentHtml').call(this); } renderFailed() { - return Model.attribute('renderFailed').call(this); + return Model.attribute('renderFailed').call(this); } contentPlain() { - return computed('contentHtml', getPlainContent as (content: unknown) => string).call(this); + return computed('contentHtml', (content) => { + if (typeof content === 'string') { + return getPlainContent(content); + } + + return content as (null | undefined); + }).call(this); } editedAt() { - return Model.attribute('editedAt', Model.transformDate).call(this); + return Model.attribute('editedAt', Model.transformDate).call(this); } editedUser() { - return Model.hasOne('editedUser').call(this); + return Model.hasOne('editedUser').call(this); } isEdited() { return computed('editedAt', (editedAt) => !!editedAt).call(this); } hiddenAt() { - return Model.attribute('hiddenAt', Model.transformDate).call(this); + return Model.attribute('hiddenAt', Model.transformDate).call(this); } hiddenUser() { - return Model.hasOne('hiddenUser').call(this); + return Model.hasOne('hiddenUser').call(this); } isHidden() { return computed('hiddenAt', (hiddenAt) => !!hiddenAt).call(this); } canEdit() { - return Model.attribute('canEdit').call(this); + return Model.attribute('canEdit').call(this); } canHide() { - return Model.attribute('canHide').call(this); + return Model.attribute('canHide').call(this); } canDelete() { - return Model.attribute('canDelete').call(this); + return Model.attribute('canDelete').call(this); } } diff --git a/framework/core/js/src/common/models/User.ts b/framework/core/js/src/common/models/User.ts index 6d66ef458..e6ea1b265 100644 --- a/framework/core/js/src/common/models/User.ts +++ b/framework/core/js/src/common/models/User.ts @@ -6,6 +6,7 @@ import ItemList from '../utils/ItemList'; import computed from '../utils/computed'; import GroupBadge from '../components/GroupBadge'; import Mithril from 'mithril'; +import Group from './Group'; export default class User extends Model { username() { @@ -19,65 +20,65 @@ export default class User extends Model { } email() { - return Model.attribute('email').call(this); + return Model.attribute('email').call(this); } isEmailConfirmed() { - return Model.attribute('isEmailConfirmed').call(this); + return Model.attribute('isEmailConfirmed').call(this); } password() { - return Model.attribute('password').call(this); + return Model.attribute('password').call(this); } avatarUrl() { - return Model.attribute('avatarUrl').call(this); + return Model.attribute('avatarUrl').call(this); } preferences() { - return Model.attribute | null>('preferences').call(this); + return Model.attribute | null | undefined>('preferences').call(this); } groups() { - return Model.hasMany('groups').call(this); + return Model.hasMany('groups').call(this); } joinTime() { - return Model.attribute('joinTime', Model.transformDate).call(this); + return Model.attribute('joinTime', Model.transformDate).call(this); } lastSeenAt() { - return Model.attribute('lastSeenAt', Model.transformDate).call(this); + return Model.attribute('lastSeenAt', Model.transformDate).call(this); } markedAllAsReadAt() { - return Model.attribute('markedAllAsReadAt', Model.transformDate).call(this); + return Model.attribute('markedAllAsReadAt', Model.transformDate).call(this); } unreadNotificationCount() { - return Model.attribute('unreadNotificationCount').call(this); + return Model.attribute('unreadNotificationCount').call(this); } newNotificationCount() { - return Model.attribute('newNotificationCount').call(this); + return Model.attribute('newNotificationCount').call(this); } discussionCount() { - return Model.attribute('discussionCount').call(this); + return Model.attribute('discussionCount').call(this); } commentCount() { - return Model.attribute('commentCount').call(this); + return Model.attribute('commentCount').call(this); } canEdit() { - return Model.attribute('canEdit').call(this); + return Model.attribute('canEdit').call(this); } canEditCredentials() { - return Model.attribute('canEditCredentials').call(this); + return Model.attribute('canEditCredentials').call(this); } canEditGroups() { - return Model.attribute('canEditGroups').call(this); + return Model.attribute('canEditGroups').call(this); } canDelete() { - return Model.attribute('canDelete').call(this); + return Model.attribute('canDelete').call(this); } color() { @@ -148,7 +149,7 @@ export default class User extends Model { m.redraw(); }; image.crossOrigin = 'anonymous'; - image.src = this.avatarUrl(); + image.src = this.avatarUrl() ?? ''; } /** diff --git a/framework/core/js/src/forum/components/DiscussionsSearchSource.tsx b/framework/core/js/src/forum/components/DiscussionsSearchSource.tsx index 804b623ea..5807566a7 100644 --- a/framework/core/js/src/forum/components/DiscussionsSearchSource.tsx +++ b/framework/core/js/src/forum/components/DiscussionsSearchSource.tsx @@ -40,7 +40,7 @@ export default class DiscussionsSearchSource implements SearchSource {
  • {highlight(discussion.title(), query)}
    - {mostRelevantPost ?
    {highlight(mostRelevantPost.contentPlain(), query, 100)}
    : ''} + {mostRelevantPost ?
    {highlight(mostRelevantPost.contentPlain() ?? '', query, 100)}
    : ''}
  • );