mirror of
https://github.com/wassname/talk.git
synced 2026-08-16 11:29:31 +08:00
[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:
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
Reference in New Issue
Block a user