[CORL-159, CORL-160] Stream Config Tab (#2219)

* feat: Implement stream configuration tab

* feat: split profile & configure into separate bundles

* chore: better role logic

* fix+chore: add test cases, implement expectAndFail, refactor tests

* chore: add some comments

* chore: Update src/core/client/framework/lib/form/helpers.tsx

Co-Authored-By: cvle <vinh@wikiwi.io>

* feat: support new graphql mutations/schema

* fix: ci fixes

* fix: improvement to revision loading

* fix: updated some tests

* fix: adapt client to changes

* fix: remove obsolote isClosed in UpdateStory

* ci: increase no_output_timeout for build
This commit is contained in:
Kiwi
2019-03-18 22:07:52 +01:00
committed by GitHub
parent c738d9a355
commit 9eb5afbb2b
128 changed files with 2249 additions and 1081 deletions
+27 -78
View File
@@ -1,46 +1,20 @@
import { Omit } from "talk-common/types";
import {
GQLCharCount,
GQLDisableCommenting,
GQLEmail,
GQLExternalIntegrations,
GQLAuth,
GQLFacebookAuthIntegration,
GQLGoogleAuthIntegration,
GQLKarma,
GQLLocalAuthIntegration,
GQLMODERATION_MODE,
GQLOIDCAuthIntegration,
GQLReactionConfiguration,
GQLSettings,
GQLSSOAuthIntegration,
GQLWordList,
} from "talk-server/graph/tenant/schema/__generated__/types";
export interface ModerationSettings {
export interface GlobalModerationSettings {
moderation: GQLMODERATION_MODE;
requireEmailConfirmation: boolean;
communityGuidelines: {
enabled: boolean;
content?: string;
};
questionBoxEnable: boolean;
questionBoxIcon?: string;
questionBoxContent?: string;
premodLinksEnable: boolean;
autoCloseStream: boolean;
/**
* closedTimeout is the amount of seconds from the createdAt timestamp that a
* given story will be considered closed.
*/
closedTimeout: number;
closedMessage?: string;
disableCommenting: GQLDisableCommenting;
charCount: GQLCharCount;
}
export type LocalAuthIntegration = GQLLocalAuthIntegration;
export type SSOAuthIntegration = GQLSSOAuthIntegration;
export type OIDCAuthIntegration = Omit<
GQLOIDCAuthIntegration,
"callbackURL" | "redirectURL"
@@ -57,63 +31,38 @@ export type FacebookAuthIntegration = Omit<
>;
export interface AuthIntegrations {
local: LocalAuthIntegration;
sso: SSOAuthIntegration;
local: GQLLocalAuthIntegration;
sso: GQLSSOAuthIntegration;
oidc: OIDCAuthIntegration;
google: GoogleAuthIntegration;
facebook: FacebookAuthIntegration;
}
export interface Auth {
export type Auth = Omit<GQLAuth, "integrations"> & {
/**
* integrations are the set of configurations for the variations of
* authentication solutions.
*/
integrations: AuthIntegrations;
}
};
export interface Settings extends ModerationSettings {
/**
* customCSSURL is the URL that can be specified by the Tenant to describe a
* URL that contains custom styles to be applied to the Stream.
*/
customCSSURL?: string;
/**
* editCommentWindowLength is the length of time (in seconds) after a comment
* is posted that it can still be edited by the author.
*/
editCommentWindowLength: number;
/**
* email is the set of credentials and settings associated with the
* Tenant.
*/
email: GQLEmail;
/**
* karma is the set of settings related to how user Trust and Karma are
* handled.
*/
karma: GQLKarma;
/**
* wordList stores all the banned/suspect words.
*/
wordList: GQLWordList;
/**
* Set of configured authentication integrations.
*/
auth: Auth;
/**
* Various integrations with external services.
*/
integrations: GQLExternalIntegrations;
/**
* reaction specifies the configuration for reactions.
*/
reaction: GQLReactionConfiguration;
}
export type Settings = GlobalModerationSettings &
Pick<
GQLSettings,
| "closeCommenting"
| "disableCommenting"
| "charCount"
| "email"
| "karma"
| "wordList"
| "integrations"
| "reaction"
| "editCommentWindowLength"
| "customCSSURL"
| "communityGuidelines"
> & {
/**
* Set of configured authentication integrations.
*/
auth: Auth;
};
+87 -11
View File
@@ -1,15 +1,14 @@
import { isNull, omitBy } from "lodash";
import { Db, MongoError } from "mongodb";
import uuid from "uuid";
import { Omit } from "talk-common/types";
import { DeepPartial, Omit } from "talk-common/types";
import { dotize } from "talk-common/utils/dotize";
import { DuplicateStoryURLError } from "talk-server/errors";
import { GQLStoryMetadata } from "talk-server/graph/tenant/schema/__generated__/types";
import Query, {
createConnectionOrderVariants,
createIndexFactory,
} from "talk-server/models/helpers/query";
import { ModerationSettings } from "talk-server/models/settings";
import {
GQLStoryMetadata,
GQLStorySettings,
} from "talk-server/graph/tenant/schema/__generated__/types";
import { TenantResource } from "talk-server/models/tenant";
import {
@@ -17,6 +16,10 @@ import {
ConnectionInput,
resolveConnection,
} from "../helpers/connection";
import Query, {
createConnectionOrderVariants,
createIndexFactory,
} from "../helpers/query";
import {
createEmptyCommentModerationQueueCounts,
createEmptyCommentStatusCounts,
@@ -30,6 +33,10 @@ function collection<T = Story>(mongo: Db) {
return mongo.collection<Readonly<T>>("stories");
}
export type StorySettings = DeepPartial<GQLStorySettings>;
export type StoryMetadata = GQLStoryMetadata;
export interface Story extends TenantResource {
readonly id: string;
@@ -41,7 +48,7 @@ export interface Story extends TenantResource {
/**
* metadata stores the scraped metadata from the Story page.
*/
metadata?: GQLStoryMetadata;
metadata?: StoryMetadata;
/**
* scrapedAt is the Time that the Story had it's metadata scraped at.
@@ -57,7 +64,7 @@ export interface Story extends TenantResource {
* settings provides a point where the settings can be overridden for a
* specific Story.
*/
settings?: Partial<ModerationSettings>;
settings: StorySettings;
/**
* closedAt is the date that the Story was forced closed at, or false to
@@ -69,6 +76,11 @@ export interface Story extends TenantResource {
* createdAt is the date that the Story was added to the Talk database.
*/
createdAt: Date;
/**
* premodLinksEnable will put all comments that contain links into premod.
*/
premodLinksEnable?: boolean;
}
export async function createStoryIndexes(mongo: Db) {
@@ -117,6 +129,7 @@ export async function upsertStory(
status: createEmptyCommentStatusCounts(),
moderationQueue: createEmptyCommentModerationQueueCounts(),
},
settings: {},
},
};
@@ -196,6 +209,7 @@ export async function createStory(
moderationQueue: createEmptyCommentModerationQueueCounts(),
status: createEmptyCommentStatusCounts(),
},
settings: {},
};
try {
@@ -291,13 +305,75 @@ export async function updateStory(
// Evaluate the error, if it is in regards to violating the unique index,
// then return a duplicate Story error.
if (input.url && err instanceof MongoError && err.code === 11000) {
// TODO: (wyattjoh) return better error
throw new Error("story with this url already exists");
throw new DuplicateStoryURLError(input.url);
}
throw err;
}
}
export type UpdateStorySettingsInput = StorySettings;
export async function updateStorySettings(
mongo: Db,
tenantID: string,
id: string,
input: UpdateStorySettingsInput
) {
// Only update fields that have been updated.
const update = {
$set: {
...omitBy(dotize({ settings: input }, { embedArrays: true }), isNull),
// Always update the updated at time.
updatedAt: new Date(),
},
};
const result = await collection(mongo).findOneAndUpdate(
{ id, tenantID },
update,
// False to return the updated document instead of the original
// document.
{ returnOriginal: false }
);
return result.value || null;
}
export async function openStory(mongo: Db, tenantID: string, id: string) {
const result = await collection(mongo).findOneAndUpdate(
{ id, tenantID },
{
$set: {
closedAt: false,
// Always update the updated at time.
updatedAt: new Date(),
},
},
// False to return the updated document instead of the original
// document.
{ returnOriginal: false }
);
return result.value || null;
}
export async function closeStory(mongo: Db, tenantID: string, id: string) {
const result = await collection(mongo).findOneAndUpdate(
{ id, tenantID },
{
$set: {
closedAt: new Date(),
// Always update the updated at time.
updatedAt: new Date(),
},
},
// False to return the updated document instead of the original
// document.
{ returnOriginal: false }
);
return result.value || null;
}
export async function removeStory(mongo: Db, tenantID: string, id: string) {
const result = await collection(mongo).findOneAndDelete({
+21 -29
View File
@@ -5,7 +5,10 @@ import uuid from "uuid";
import { LanguageCode } from "talk-common/helpers/i18n/locales";
import { DeepPartial, Omit, Sub } from "talk-common/types";
import { dotize } from "talk-common/utils/dotize";
import { GQLMODERATION_MODE } from "talk-server/graph/tenant/schema/__generated__/types";
import {
GQLMODERATION_MODE,
GQLSettings,
} from "talk-server/graph/tenant/schema/__generated__/types";
import { createIndexFactory } from "talk-server/models/helpers/query";
import { Settings } from "talk-server/models/settings";
@@ -13,6 +16,10 @@ function collection(mongo: Db) {
return mongo.collection<Readonly<Tenant>>("tenants");
}
/**
* TenantResource references a given resource that should be owned by a specific
* Tenant.
*/
export interface TenantResource {
/**
* tenantID is the reference to the specific Tenant that owns this particular
@@ -21,29 +28,21 @@ export interface TenantResource {
readonly tenantID: string;
}
/**
* Tenant describes a given Tenant on Talk that has Stories, Comments, and Users.
*/
export interface Tenant extends Settings {
export interface TenantSettings
extends Pick<GQLSettings, "domain" | "domains" | "organization"> {
readonly id: string;
// Domain is set when the tenant is created, and is used to retrieve the
// specific tenant that the API request pertains to.
domain: string;
// domains is the list of domains that are allowed to have the iframe load on.
domains: string[];
/**
* locale is the specified locale for this Tenant.
*/
locale: LanguageCode;
organizationName: string;
organizationURL: string;
organizationContactEmail: string;
}
/**
* Tenant describes a given Tenant on Talk that has Stories, Comments, and Users.
*/
export type Tenant = Settings & TenantSettings;
export async function createTenantIndexes(mongo: Db) {
const createIndex = createIndexFactory(collection(mongo));
@@ -61,12 +60,7 @@ export async function createTenantIndexes(mongo: Db) {
*/
export type CreateTenantInput = Pick<
Tenant,
| "domain"
| "domains"
| "locale"
| "organizationName"
| "organizationURL"
| "organizationContactEmail"
"domain" | "domains" | "locale" | "organization"
>;
/**
@@ -83,22 +77,20 @@ export async function createTenant(mongo: Db, input: CreateTenantInput) {
// Default to post moderation.
moderation: GQLMODERATION_MODE.POST,
// Email confirmation is default off.
requireEmailConfirmation: false,
communityGuidelines: {
enabled: false,
content: "",
},
questionBoxEnable: false,
premodLinksEnable: false,
autoCloseStream: false,
closeCommenting: {
auto: false,
// 2 weeks timeout.
timeout: 60 * 60 * 24 * 7 * 2,
},
disableCommenting: {
enabled: false,
},
// 2 weeks timeout.
closedTimeout: 60 * 60 * 24 * 7 * 2,
// 30 seconds edit window length.
editCommentWindowLength: 30,