[CORL 547] org-wide announcements (#2813)

* CRUD announcements

* only show announcement if not disabled

* make announcements dismissable

* add announcement mutations

* update announcement form logic

* style announcements on stream

* update snap

* localize strings

* close form if announcement is removed

* move announcement config below sitewide commenting config

* move date calculation inside useMemo

* move announcementconfig code to announcementconfigcontainer

* use coralContext for localStorage

* fix type of announcement createdAt

* move announcement form to modal

* remove payload pruning from configure route

* simplify announcement display logic

* make validation message full width

Co-authored-by: Kim Gardner <kgardnr@gmail.com>
This commit is contained in:
Tessa Thornton
2020-02-03 13:12:25 -05:00
committed by GitHub
co-authored by Kim Gardner
parent a7b2af85fc
commit a1a8652f7e
29 changed files with 855 additions and 1 deletions
@@ -1,6 +1,8 @@
import GraphContext from "coral-server/graph/context";
import { Tenant } from "coral-server/models/tenant";
import {
createAnnouncement,
deleteAnnouncement,
disableFeatureFlag,
enableFeatureFlag,
regenerateSSOKey,
@@ -8,6 +10,7 @@ import {
} from "coral-server/services/tenant";
import {
GQLCreateAnnouncementInput,
GQLFEATURE_FLAG,
GQLUpdateSettingsInput,
} from "coral-server/graph/schema/__generated__/types";
@@ -28,4 +31,8 @@ export const Settings = ({
enableFeatureFlag(mongo, redis, tenantCache, tenant, flag),
disableFeatureFlag: (flag: GQLFEATURE_FLAG) =>
disableFeatureFlag(mongo, redis, tenantCache, tenant, flag),
createAnnouncement: (input: GQLCreateAnnouncementInput) =>
createAnnouncement(mongo, redis, tenantCache, tenant, input, now),
deleteAnnouncement: () =>
deleteAnnouncement(mongo, redis, tenantCache, tenant),
});
@@ -237,4 +237,12 @@ export const Mutation: Required<GQLMutationTypeResolver<void>> = {
flags: await ctx.mutators.Settings.disableFeatureFlag(input.flag),
clientMutationId: input.clientMutationId,
}),
createAnnouncement: async (source, { input }, ctx) => ({
settings: await ctx.mutators.Settings.createAnnouncement(input),
clientMutationId: input.clientMutationId,
}),
deleteAnnouncement: async (source, { input }, ctx) => ({
settings: await ctx.mutators.Settings.deleteAnnouncement(),
clientMutationId: input.clientMutationId,
}),
};
+6 -1
View File
@@ -1,4 +1,7 @@
import { Tenant } from "coral-server/models/tenant";
import {
retrieveAnnouncementIfEnabled,
Tenant,
} from "coral-server/models/tenant";
import {
GQLFEATURE_FLAG,
@@ -18,4 +21,6 @@ export const Settings: GQLSettingsTypeResolver<Tenant> = {
slack: ({ slack = {} }) => slack,
featureFlags: ({ featureFlags = [] }) =>
featureFlags.filter(filterValidFeatureFlags()),
announcement: ({ announcement }) =>
retrieveAnnouncementIfEnabled(announcement),
};
@@ -1215,6 +1215,37 @@ type NewCommentersConfiguration {
approvedCommentsThreshold: Int!
}
"""
Announcement is an organization-wide announcement displayed above the stream
for a set duration
"""
type Announcement {
"""
id is a canonical identifier for this Annoucnement, used to dismiss on front-end.
"""
id: ID!
"""
createdAt is the creation date.
"""
createdAt: Time!
"""
disableAt is the computed date at which announcement will be invalid.
"""
disableAt: Time!
"""
duration determines how long the announcement will be valid for.
"""
duration: Int!
"""
content is the content displayed for the announcement.
"""
content: String!
}
"""
Settings stores the global settings for a given Tenant.
"""
@@ -1365,6 +1396,8 @@ type Settings {
newCommenters is the configuration for how new commenters comments are treated.
"""
newCommenters: NewCommentersConfiguration! @auth(roles: [ADMIN])
announcement: Announcement
}
################################################################################
@@ -3353,6 +3386,44 @@ input RecentCommentHistoryConfigurationInput {
triggerRejectionRate: Float
}
input CreateAnnouncementInput {
"""
content of the announcement.
"""
content: String!
duration: Int!
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
}
input DeleteAnnouncementInput {
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
}
type CreateAnnouncementPayload {
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
settings: Settings!
}
type DeleteAnnouncementPayload {
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
settings: Settings!
}
input SettingsCommunityGuidelinesInput {
"""
enable set to true will show the guidelines above the message box.
@@ -5763,6 +5834,14 @@ type Mutation {
disableFeatureFlag(
input: DisableFeatureFlagInput!
): DisableFeatureFlagPayload! @auth(roles: [ADMIN])
createAnnouncement(
input: CreateAnnouncementInput!
): CreateAnnouncementPayload! @auth(roles: [ADMIN])
deleteAnnouncement(
input: DeleteAnnouncementInput!
): DeleteAnnouncementPayload! @auth(roles: [ADMIN])
}
##################
+1
View File
@@ -146,6 +146,7 @@ export type Settings = GlobalModerationSettings &
| "stories"
| "createdAt"
| "slack"
| "announcement"
> & {
/**
* auth is the set of configured authentication integrations.
+67
View File
@@ -1,4 +1,5 @@
import { isEmpty } from "lodash";
import { DateTime } from "luxon";
import { Db } from "mongodb";
import uuid from "uuid";
@@ -6,12 +7,14 @@ import { DEFAULT_SESSION_DURATION } from "coral-common/constants";
import { LanguageCode } from "coral-common/helpers/i18n/locales";
import TIME from "coral-common/time";
import { DeepPartial, Omit, Sub } from "coral-common/types";
import { isBeforeDate } from "coral-common/utils";
import { dotize } from "coral-common/utils/dotize";
import { Settings } from "coral-server/models/settings";
import { I18n } from "coral-server/services/i18n";
import { tenants as collection } from "coral-server/services/mongodb/collections";
import {
GQLAnnouncement,
GQLFEATURE_FLAG,
GQLMODERATION_MODE,
GQLSettings,
@@ -396,3 +399,67 @@ export async function disableTenantFeatureFlag(
return result.value || null;
}
export interface CreateAnnouncementInput {
content: string;
duration: number;
}
export async function createTenantAnnouncement(
mongo: Db,
id: string,
input: CreateAnnouncementInput,
now = new Date()
) {
const announcement = {
id: uuid.v4(),
...input,
createdAt: now,
};
const result = await collection(mongo).findOneAndUpdate(
{ id },
{
$set: {
announcement,
},
},
{
returnOriginal: false,
}
);
return result.value;
}
export async function deleteTenantAnnouncement(mongo: Db, id: string) {
const result = await collection(mongo).findOneAndUpdate(
{ id },
{
$unset: {
announcement: "",
},
},
{
returnOriginal: false,
}
);
return result.value;
}
export function retrieveAnnouncementIfEnabled(
announcement?: GQLAnnouncement | null
) {
if (!announcement) {
return null;
}
const disableAt = DateTime.fromJSDate(announcement.createdAt)
.plus({ seconds: announcement.duration })
.toJSDate();
if (isBeforeDate(disableAt)) {
return {
...announcement,
disableAt,
};
}
return null;
}
+33
View File
@@ -9,9 +9,12 @@ import { Config } from "coral-server/config";
import { TenantInstalledAlreadyError } from "coral-server/errors";
import logger from "coral-server/logger";
import {
CreateAnnouncementInput,
createTenant,
createTenantAnnouncement,
CreateTenantInput,
createTenantSSOKey,
deleteTenantAnnouncement,
disableTenantFeatureFlag,
enableTenantFeatureFlag,
rotateTenantSSOKey,
@@ -253,3 +256,33 @@ export async function disableFeatureFlag(
// begin with).
return updated.featureFlags || [];
}
export async function createAnnouncement(
mongo: Db,
redis: Redis,
cache: TenantCache,
tenant: Tenant,
input: CreateAnnouncementInput,
now = new Date()
) {
const updated = await createTenantAnnouncement(mongo, tenant.id, input);
if (!updated) {
throw new Error("tenant not found");
}
await cache.update(redis, updated);
return updated;
}
export async function deleteAnnouncement(
mongo: Db,
redis: Redis,
cache: TenantCache,
tenant: Tenant
) {
const updated = await deleteTenantAnnouncement(mongo, tenant.id);
if (!updated) {
throw new Error("tenant not found");
}
await cache.update(redis, updated);
return updated;
}