Drop ignoreHooks configuration. (#592)

* Refactor HookFilter to only support enabledEvents (and add a function to convert)

* Convert connections to deprecate ignoreHooks

* Update documentation

* Split out EventHookCheckbox

* Refactor frontend to support enableHooks only mode

* drop old field name

* changelog

* Fix enabledHooks for widgets

* Fixes across the board

* Update test description

* Cleanup

* Fix HookFilter

* Fixup checkboxes

* Cleanup
This commit is contained in:
Will Hunt 2023-01-10 17:08:50 +00:00 committed by GitHub
parent 4048cc8b01
commit 1e8a112a28
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 338 additions and 286 deletions

2
changelog.d/592.feature Normal file
View File

@ -0,0 +1,2 @@
The GitHub/GitLab connection state configuration has changed. The configuration option `ignoreHooks` is now deprecated, and new connections may not use this options.
Users should instead explicitly configure all the hooks they want to enable with the `enableHooks` option. Existing connections will continue to work with both options.

View File

@ -27,8 +27,8 @@ This connection supports a few options which can be defined in the room state:
| Option | Description | Allowed values | Default | | Option | Description | Allowed values | Default |
|--------|-------------|----------------|---------| |--------|-------------|----------------|---------|
|enableHooks [^1]|Enable notifications for some event types|Array of: [Supported event types](#supported-event-types) |*empty*| |enableHooks [^1]|Enable notifications for some event types|Array of: [Supported event types](#supported-event-types) |If not defined, defaults are mentioned below|
|ignoreHooks [^1]|Choose to exclude notifications for some event types|Array of: [Supported event types](#supported-event-types) |*empty*| |ignoreHooks [^1]|**deprecated** Choose to exclude notifications for some event types|Array of: [Supported event types](#supported-event-types) |*empty*|
|commandPrefix|Choose the prefix to use when sending commands to the bot|A string, ideally starts with "!"|`!gh`| |commandPrefix|Choose the prefix to use when sending commands to the bot|A string, ideally starts with "!"|`!gh`|
|showIssueRoomLink|When new issues are created, provide a Matrix alias link to the issue room|`true/false`|`false`| |showIssueRoomLink|When new issues are created, provide a Matrix alias link to the issue room|`true/false`|`false`|
|prDiff|Show a diff in the room when a PR is created, subject to limits|`{enabled: boolean, maxLines: number}`|`{enabled: false}`| |prDiff|Show a diff in the room when a PR is created, subject to limits|`{enabled: boolean, maxLines: number}`|`{enabled: false}`|
@ -43,14 +43,16 @@ This connection supports a few options which can be defined in the room state:
|workflowRun.excludingWorkflows|Never report workflow runs with a matching workflow name.|Array of: String matching a workflow name|*empty*| |workflowRun.excludingWorkflows|Never report workflow runs with a matching workflow name.|Array of: String matching a workflow name|*empty*|
[^1]: `ignoreHooks` takes precedence over `enableHooks`. [^1]: `ignoreHooks` is no longer accepted for new state events. Use `enableHooks` to explicitly state all events you want to see.
### Supported event types ### Supported event types
This connection supports sending messages when the following actions happen on the repository. This connection supports sending messages when the following actions happen on the repository.
Note: Some of these event types are enabled by default (marked with a `*`) Note: Some of these event types are enabled by default (marked with a `*`). When `ignoreHooks` *is* defined,
the events marked as default below will be enabled. Otherwise, this is ignored.
- issue * - issue *
- issue.created * - issue.created *

View File

@ -23,26 +23,33 @@ This connection supports a few options which can be defined in the room state:
| Option | Description | Allowed values | Default | | Option | Description | Allowed values | Default |
|--------|-------------|----------------|---------| |--------|-------------|----------------|---------|
|ignoreHooks|Choose to exclude notifications for some event types|Array of: [Supported event types](#supported-event-types) |*empty*|
|commandPrefix|Choose the prefix to use when sending commands to the bot|A string, ideally starts with "!"|`!gh`| |commandPrefix|Choose the prefix to use when sending commands to the bot|A string, ideally starts with "!"|`!gh`|
|pushTagsRegex|Only mention pushed tags which match this regex|Regex string|*empty*| |enableHooks [^1]|Enable notifications for some event types|Array of: [Supported event types](#supported-event-types) |If not defined, defaults are mentioned below|
|includingLabels|Only notify on issues matching these label names|Array of: String matching a label name|*empty*|
|excludingLabels|Never notify on issues matching these label names|Array of: String matching a label name|*empty*| |excludingLabels|Never notify on issues matching these label names|Array of: String matching a label name|*empty*|
|ignoreHooks [^1]|**deprecated** Choose to exclude notifications for some event types|Array of: [Supported event types](#supported-event-types) |*empty*|
|includeCommentBody|Include the body of a comment when notifying on merge requests|Boolean|false| |includeCommentBody|Include the body of a comment when notifying on merge requests|Boolean|false|
|includingLabels|Only notify on issues matching these label names|Array of: String matching a label name|*empty*|
|pushTagsRegex|Only mention pushed tags which match this regex|Regex string|*empty*|
[^1]: `ignoreHooks` is no longer accepted for new state events. Use `enableHooks` to explicitly state all events you want to see.
### Supported event types ### Supported event types
This connection supports sending messages when the following actions happen on the repository. This connection supports sending messages when the following actions happen on the repository.
- merge_request Note: Some of these event types are enabled by default (marked with a `*`). When `ignoreHooks` *is* defined,
- merge_request.close the events marked as default below will be enabled. Otherwise, this is ignored.
- merge_request.merge
- merge_request.open - merge_request *
- merge_request.review.comments - merge_request.close *
- merge_request.review - merge_request.merge *
- push - merge_request.open *
- release - merge_request.review.comments *
- release.created - merge_request.review *
- tag_push - push *
- wiki - release *
- release.created *
- tag_push *
- wiki *

View File

@ -10,14 +10,14 @@ const log = new Logger("CommandConnection");
* Connection class that handles commands for a given connection. Should be used * Connection class that handles commands for a given connection. Should be used
* by connections expecting to handle user input. * by connections expecting to handle user input.
*/ */
export abstract class CommandConnection<StateType extends IConnectionState = IConnectionState> extends BaseConnection { export abstract class CommandConnection<StateType extends IConnectionState = IConnectionState, ValidatedStateType extends StateType = StateType> extends BaseConnection {
protected enabledHelpCategories?: string[]; protected enabledHelpCategories?: string[];
protected includeTitlesInHelp?: boolean; protected includeTitlesInHelp?: boolean;
constructor( constructor(
roomId: string, roomId: string,
stateKey: string, stateKey: string,
canonicalStateType: string, canonicalStateType: string,
protected state: StateType, protected state: ValidatedStateType,
private readonly botClient: MatrixClient, private readonly botClient: MatrixClient,
private readonly botCommands: BotCommands, private readonly botCommands: BotCommands,
private readonly helpMessage: HelpFunction, private readonly helpMessage: HelpFunction,
@ -39,7 +39,7 @@ export abstract class CommandConnection<StateType extends IConnectionState = ICo
this.state = this.validateConnectionState(stateEv.content); this.state = this.validateConnectionState(stateEv.content);
} }
protected abstract validateConnectionState(content: unknown): StateType; protected abstract validateConnectionState(content: unknown): ValidatedStateType;
public async onMessageEvent(ev: MatrixEvent<MatrixMessageContent>, checkPermission: PermissionCheckFn) { public async onMessageEvent(ev: MatrixEvent<MatrixMessageContent>, checkPermission: PermissionCheckFn) {
const commandResult = await handleCommand( const commandResult = await handleCommand(

View File

@ -40,8 +40,12 @@ interface IQueryRoomOpts {
} }
export interface GitHubRepoConnectionOptions extends IConnectionState { export interface GitHubRepoConnectionOptions extends IConnectionState {
enableHooks?: AllowedEventsNames[], /**
* Do not use. Use `enableHooks`.
* @deprecated
*/
ignoreHooks?: AllowedEventsNames[], ignoreHooks?: AllowedEventsNames[],
enableHooks?: AllowedEventsNames[],
showIssueRoomLink?: boolean; showIssueRoomLink?: boolean;
prDiff?: { prDiff?: {
enabled: boolean; enabled: boolean;
@ -61,11 +65,17 @@ export interface GitHubRepoConnectionOptions extends IConnectionState {
excludingWorkflows?: string[]; excludingWorkflows?: string[];
} }
} }
export interface GitHubRepoConnectionState extends GitHubRepoConnectionOptions { export interface GitHubRepoConnectionState extends GitHubRepoConnectionOptions {
org: string; org: string;
repo: string; repo: string;
} }
interface ConnectionValidatedState extends GitHubRepoConnectionState {
ignoreHooks: undefined,
enableHooks: AllowedEventsNames[],
}
export interface GitHubRepoConnectionOrgTarget { export interface GitHubRepoConnectionOrgTarget {
name: string; name: string;
@ -136,8 +146,17 @@ const AllowedEvents: AllowedEventsNames[] = [
* These hooks are enabled by default, unless they are * These hooks are enabled by default, unless they are
* specifed in the ignoreHooks option. * specifed in the ignoreHooks option.
*/ */
const AllowHookByDefault: AllowedEventsNames[] = [ const DefaultHooks: AllowedEventsNames[] = [
"issue.changed",
"issue.created",
"issue.edited",
"issue.labeled",
"issue", "issue",
"pull_request.closed",
"pull_request.merged",
"pull_request.opened",
"pull_request.ready_for_review",
"pull_request.reviewed",
"pull_request", "pull_request",
"release.created" "release.created"
]; ];
@ -151,6 +170,10 @@ const ConnectionStateSchema = {
}, },
org: {type: "string"}, org: {type: "string"},
repo: {type: "string"}, repo: {type: "string"},
/**
* Legacy state.
* @deprecated
*/
ignoreHooks: { ignoreHooks: {
type: "array", type: "array",
items: { items: {
@ -310,19 +333,25 @@ export interface GitHubTargetFilter {
* Handles rooms connected to a GitHub repo. * Handles rooms connected to a GitHub repo.
*/ */
@Connection @Connection
export class GitHubRepoConnection extends CommandConnection<GitHubRepoConnectionState> implements IConnection { export class GitHubRepoConnection extends CommandConnection<GitHubRepoConnectionState, ConnectionValidatedState> implements IConnection {
static validateState(state: unknown, isExistingState = false): ConnectionValidatedState {
static validateState(state: unknown, isExistingState = false): GitHubRepoConnectionState {
const validator = new Ajv({ allowUnionTypes: true }).compile(ConnectionStateSchema); const validator = new Ajv({ allowUnionTypes: true }).compile(ConnectionStateSchema);
if (validator(state)) { if (validator(state)) {
// Validate ignoreHooks IF this is an incoming update (we can be less strict for existing state)
if (!isExistingState && state.ignoreHooks && !state.ignoreHooks.every(h => AllowedEvents.includes(h))) {
throw new ApiError('`ignoreHooks` must only contain allowed values', ErrCode.BadValue);
}
if (!isExistingState && state.enableHooks && !state.enableHooks.every(h => AllowedEvents.includes(h))) { if (!isExistingState && state.enableHooks && !state.enableHooks.every(h => AllowedEvents.includes(h))) {
throw new ApiError('`enableHooks` must only contain allowed values', ErrCode.BadValue); throw new ApiError('`enableHooks` must only contain allowed values', ErrCode.BadValue);
} }
return state; if (state.ignoreHooks) {
if (!isExistingState) {
throw new ApiError('`ignoreHooks` cannot be used with new connections', ErrCode.BadValue);
}
log.warn(`Room has old state key 'ignoreHooks'. Converting to compatible enabledHooks filter`);
state.enableHooks = HookFilter.convertIgnoredHooksToEnabledHooks(state.enableHooks, state.ignoreHooks, DefaultHooks);
}
return {
...state,
ignoreHooks: undefined,
enableHooks: state.enableHooks ?? [...DefaultHooks]
};
} }
throw new ValidatorApiError(validator.errors); throw new ValidatorApiError(validator.errors);
} }
@ -471,27 +500,25 @@ export class GitHubRepoConnection extends CommandConnection<GitHubRepoConnection
constructor(roomId: string, constructor(roomId: string,
private readonly as: Appservice, private readonly as: Appservice,
state: GitHubRepoConnectionState, state: ConnectionValidatedState,
private readonly tokenStore: UserTokenStore, private readonly tokenStore: UserTokenStore,
stateKey: string, stateKey: string,
private readonly githubInstance: GithubInstance, private readonly githubInstance: GithubInstance,
private readonly config: BridgeConfigGitHub, private readonly config: BridgeConfigGitHub,
) { ) {
super( super(
roomId, roomId,
stateKey, stateKey,
GitHubRepoConnection.CanonicalEventType, GitHubRepoConnection.CanonicalEventType,
state, state,
as.botClient, as.botClient,
GitHubRepoConnection.botCommands, GitHubRepoConnection.botCommands,
GitHubRepoConnection.helpMessage, GitHubRepoConnection.helpMessage,
"!gh", "!gh",
"github", "github",
); );
this.hookFilter = new HookFilter( this.hookFilter = new HookFilter(
AllowHookByDefault,
state.enableHooks, state.enableHooks,
state.ignoreHooks,
) )
} }
@ -530,8 +557,7 @@ export class GitHubRepoConnection extends CommandConnection<GitHubRepoConnection
public async onStateUpdate(stateEv: MatrixEvent<unknown>) { public async onStateUpdate(stateEv: MatrixEvent<unknown>) {
await super.onStateUpdate(stateEv); await super.onStateUpdate(stateEv);
this.hookFilter.enabledHooks = this.state.enableHooks ?? []; this.hookFilter.enabledHooks = this.state.enableHooks;
this.hookFilter.ignoredHooks = this.state.ignoreHooks ?? [];
} }
public isInterestedInStateEvent(eventType: string, stateKey: string) { public isInterestedInStateEvent(eventType: string, stateKey: string) {
@ -1307,7 +1333,6 @@ export class GitHubRepoConnection extends CommandConnection<GitHubRepoConnection
await this.as.botClient.sendStateEvent(this.roomId, GitHubRepoConnection.CanonicalEventType, this.stateKey, validatedConfig); await this.as.botClient.sendStateEvent(this.roomId, GitHubRepoConnection.CanonicalEventType, this.stateKey, validatedConfig);
this.state = validatedConfig; this.state = validatedConfig;
this.hookFilter.enabledHooks = this.state.enableHooks ?? []; this.hookFilter.enabledHooks = this.state.enableHooks ?? [];
this.hookFilter.ignoredHooks = this.state.ignoreHooks ?? [];
} }
public async onRemove() { public async onRemove() {

View File

@ -21,6 +21,11 @@ import { HookFilter } from "../HookFilter";
export interface GitLabRepoConnectionState extends IConnectionState { export interface GitLabRepoConnectionState extends IConnectionState {
instance: string; instance: string;
path: string; path: string;
enableHooks?: AllowedEventsNames[],
/**
* Do not use. Use `enableHooks`
* @deprecated
*/
ignoreHooks?: AllowedEventsNames[], ignoreHooks?: AllowedEventsNames[],
includeCommentBody?: boolean; includeCommentBody?: boolean;
pushTagsRegex?: string, pushTagsRegex?: string,
@ -28,6 +33,11 @@ export interface GitLabRepoConnectionState extends IConnectionState {
excludingLabels?: string[]; excludingLabels?: string[];
} }
interface ConnectionStateValidated extends GitLabRepoConnectionState {
ignoreHooks: undefined,
enableHooks: AllowedEventsNames[],
}
export interface GitLabRepoConnectionInstanceTarget { export interface GitLabRepoConnectionInstanceTarget {
name: string; name: string;
@ -80,6 +90,8 @@ const AllowedEvents: AllowedEventsNames[] = [
"release.created", "release.created",
]; ];
const DefaultHooks = AllowedEvents;
const ConnectionStateSchema = { const ConnectionStateSchema = {
type: "object", type: "object",
properties: { properties: {
@ -89,6 +101,10 @@ const ConnectionStateSchema = {
}, },
instance: { type: "string" }, instance: { type: "string" },
path: { type: "string" }, path: { type: "string" },
/**
* Do not use. Use `enableHooks`
* @deprecated
*/
ignoreHooks: { ignoreHooks: {
type: "array", type: "array",
items: { items: {
@ -96,6 +112,13 @@ const ConnectionStateSchema = {
}, },
nullable: true, nullable: true,
}, },
enableHooks: {
type: "array",
items: {
type: "string",
},
nullable: true,
},
commandPrefix: { commandPrefix: {
type: "string", type: "string",
minLength: 2, minLength: 2,
@ -139,7 +162,7 @@ export interface GitLabTargetFilter {
* Handles rooms connected to a GitLab repo. * Handles rooms connected to a GitLab repo.
*/ */
@Connection @Connection
export class GitLabRepoConnection extends CommandConnection<GitLabRepoConnectionState> implements IConnection { export class GitLabRepoConnection extends CommandConnection<GitLabRepoConnectionState, ConnectionStateValidated> implements IConnection {
static readonly CanonicalEventType = "uk.half-shot.matrix-hookshot.gitlab.repository"; static readonly CanonicalEventType = "uk.half-shot.matrix-hookshot.gitlab.repository";
static readonly LegacyCanonicalEventType = "uk.half-shot.matrix-github.gitlab.repository"; static readonly LegacyCanonicalEventType = "uk.half-shot.matrix-github.gitlab.repository";
@ -152,14 +175,25 @@ export class GitLabRepoConnection extends CommandConnection<GitLabRepoConnection
static helpMessage: (cmdPrefix?: string | undefined) => MatrixMessageContent; static helpMessage: (cmdPrefix?: string | undefined) => MatrixMessageContent;
static ServiceCategory = "gitlab"; static ServiceCategory = "gitlab";
static validateState(state: unknown, isExistingState = false): GitLabRepoConnectionState { static validateState(state: unknown, isExistingState = false): ConnectionStateValidated {
const validator = new Ajv({ strict: false }).compile(ConnectionStateSchema); const validator = new Ajv({ strict: false }).compile(ConnectionStateSchema);
if (validator(state)) { if (validator(state)) {
// Validate ignoreHooks IF this is an incoming update (we can be less strict for existing state) // Validate enableHooks IF this is an incoming update (we can be less strict for existing state)
if (!isExistingState && state.ignoreHooks && !state.ignoreHooks.every(h => AllowedEvents.includes(h))) { if (!isExistingState && state.enableHooks && !state.enableHooks.every(h => AllowedEvents.includes(h))) {
throw new ApiError('`ignoreHooks` must only contain allowed values', ErrCode.BadValue); throw new ApiError('`enableHooks` must only contain allowed values', ErrCode.BadValue);
} }
return state; if (state.ignoreHooks) {
if (!isExistingState) {
throw new ApiError('`ignoreHooks` cannot be used with new connections', ErrCode.BadValue);
}
log.warn(`Room has old state key 'ignoreHooks'. Converting to compatible enabledHooks filter`);
state.enableHooks = HookFilter.convertIgnoredHooksToEnabledHooks(state.enableHooks, state.ignoreHooks, AllowedEvents);
}
return {
...state,
enableHooks: state.enableHooks ?? AllowedEvents,
ignoreHooks: undefined,
};
} }
throw new ValidatorApiError(validator.errors); throw new ValidatorApiError(validator.errors);
} }
@ -309,30 +343,28 @@ export class GitLabRepoConnection extends CommandConnection<GitLabRepoConnection
constructor(roomId: string, constructor(roomId: string,
stateKey: string, stateKey: string,
private readonly as: Appservice, private readonly as: Appservice,
state: GitLabRepoConnectionState, state: ConnectionStateValidated,
private readonly tokenStore: UserTokenStore, private readonly tokenStore: UserTokenStore,
private readonly instance: GitLabInstance) { private readonly instance: GitLabInstance
super( ) {
roomId, super(
stateKey, roomId,
GitLabRepoConnection.CanonicalEventType, stateKey,
state, GitLabRepoConnection.CanonicalEventType,
as.botClient, state,
GitLabRepoConnection.botCommands, as.botClient,
GitLabRepoConnection.helpMessage, GitLabRepoConnection.botCommands,
"!gl", GitLabRepoConnection.helpMessage,
"gitlab", "!gl",
) "gitlab",
if (!state.path || !state.instance) { )
throw Error('Invalid state, missing `path` or `instance`'); if (!state.path || !state.instance) {
} throw Error('Invalid state, missing `path` or `instance`');
this.hookFilter = new HookFilter( }
// GitLab allows all events by default this.hookFilter = new HookFilter(
AllowedEvents, state.enableHooks ?? DefaultHooks,
[], );
state.ignoreHooks, }
);
}
public get path() { public get path() {
return this.state.path.toLowerCase(); return this.state.path.toLowerCase();
@ -361,7 +393,7 @@ export class GitLabRepoConnection extends CommandConnection<GitLabRepoConnection
public async onStateUpdate(stateEv: MatrixEvent<unknown>) { public async onStateUpdate(stateEv: MatrixEvent<unknown>) {
await super.onStateUpdate(stateEv); await super.onStateUpdate(stateEv);
this.hookFilter.ignoredHooks = this.state.ignoreHooks ?? []; this.hookFilter.enabledHooks = this.state.enableHooks;
} }
public getProvisionerDetails(): GitLabRepoResponseItem { public getProvisionerDetails(): GitLabRepoResponseItem {
@ -780,7 +812,7 @@ ${data.description}`;
const validatedConfig = GitLabRepoConnection.validateState(config); const validatedConfig = GitLabRepoConnection.validateState(config);
await this.as.botClient.sendStateEvent(this.roomId, GitLabRepoConnection.CanonicalEventType, this.stateKey, validatedConfig); await this.as.botClient.sendStateEvent(this.roomId, GitLabRepoConnection.CanonicalEventType, this.stateKey, validatedConfig);
this.state = validatedConfig; this.state = validatedConfig;
this.hookFilter.ignoredHooks = this.state.ignoreHooks ?? []; this.hookFilter.enabledHooks = this.state.enableHooks;
} }
public async onRemove() { public async onRemove() {

View File

@ -81,7 +81,7 @@ export interface ConnectionDeclaration<C extends IConnection = IConnection> {
EventTypes: string[]; EventTypes: string[];
ServiceCategory: string; ServiceCategory: string;
provisionConnection?: (roomId: string, userId: string, data: Record<string, unknown>, opts: ProvisionConnectionOpts) => Promise<{connection: C, warning?: ConnectionWarning}>; provisionConnection?: (roomId: string, userId: string, data: Record<string, unknown>, opts: ProvisionConnectionOpts) => Promise<{connection: C, warning?: ConnectionWarning}>;
createConnectionForState: (roomId: string, state: StateEvent<Record<string, unknown>>, opts: InstantiateConnectionOpts) => C|Promise<C> createConnectionForState: (roomId: string, state: StateEvent<Record<string, unknown>>, opts: InstantiateConnectionOpts) => C|Promise<C>;
} }
export const ConnectionDeclarations: Array<ConnectionDeclaration> = []; export const ConnectionDeclarations: Array<ConnectionDeclaration> = [];

View File

@ -106,7 +106,7 @@ export class JiraProjectConnection extends CommandConnection<JiraProjectConnecti
static botCommands: BotCommands; static botCommands: BotCommands;
static helpMessage: (cmdPrefix?: string) => MatrixMessageContent; static helpMessage: (cmdPrefix?: string) => MatrixMessageContent;
static async provisionConnection(roomId: string, userId: string, data: Record<string, unknown>, {getAllConnectionsOfType, as, tokenStore, config}: ProvisionConnectionOpts) { static async provisionConnection(roomId: string, userId: string, data: Record<string, unknown>, {as, tokenStore, config}: ProvisionConnectionOpts) {
if (!config.jira) { if (!config.jira) {
throw new ApiError('JIRA integration is not configured', ErrCode.DisabledFeature); throw new ApiError('JIRA integration is not configured', ErrCode.DisabledFeature);
} }

View File

@ -1,19 +1,32 @@
export class HookFilter<T extends string> { export class HookFilter<T extends string> {
static convertIgnoredHooksToEnabledHooks<T extends string>(explicitlyEnabledHooks: T[] = [], ignoredHooks: T[], defaultHooks: T[]): T[] {
const resultHookSet = new Set([
...explicitlyEnabledHooks,
...defaultHooks,
]);
// For each ignored hook, remove anything that matches.
for (const ignoredHook of ignoredHooks) {
resultHookSet.delete(ignoredHook);
// If the hook is a "root" hook name, remove all children.
for (const enabledHook of resultHookSet) {
if (enabledHook.startsWith(`${ignoredHook}.`)) {
resultHookSet.delete(enabledHook);
}
}
}
return [...resultHookSet];
}
constructor( constructor(
public readonly defaultHooks: T[],
public enabledHooks: T[] = [], public enabledHooks: T[] = [],
public ignoredHooks: T[] = []
) { ) {
} }
public shouldSkip(...hookName: T[]) { public shouldSkip(...hookName: T[]) {
if (hookName.some(name => this.ignoredHooks.includes(name))) { // Should skip if all of the hook names are missing
return true; return hookName.every(name => !this.enabledHooks.includes(name));
}
if (hookName.some(name => this.enabledHooks.includes(name))) {
return false;
}
return !hookName.some(h => this.defaultHooks.includes(h));
} }
} }

View File

@ -3,29 +3,51 @@ import { HookFilter } from '../src/HookFilter';
const DEFAULT_SET = ['default-allowed', 'default-allowed-but-ignored']; const DEFAULT_SET = ['default-allowed', 'default-allowed-but-ignored'];
const ENABLED_SET = ['enabled-hook', 'enabled-but-ignored']; const ENABLED_SET = ['enabled-hook', 'enabled-but-ignored'];
const IGNORED_SET = ['ignored', 'enabled-but-ignored', 'default-allowed-but-ignored'];
describe("HookFilter", () => { describe("HookFilter", () => {
let filter: HookFilter<string>; let filter: HookFilter<string>;
beforeEach(() => { beforeEach(() => {
filter = new HookFilter(DEFAULT_SET, ENABLED_SET, IGNORED_SET); filter = new HookFilter(ENABLED_SET);
}); });
it('should skip a hook named in ignoreHooks', () => { describe('shouldSkip', () => {
expect(filter.shouldSkip('ignored')).to.be.true; it('should allow a hook named in enabled set', () => {
expect(filter.shouldSkip('enabled-hook')).to.be.false;
});
it('should not allow a hook not named in enabled set', () => {
expect(filter.shouldSkip('not-enabled-hook')).to.be.true;
});
}); });
it('should allow a hook named in defaults', () => {
expect(filter.shouldSkip('default-allowed')).to.be.false; describe('convertIgnoredHooksToEnabledHooks', () => {
}); it('should correctly provide a list of default hooks', () => {
it('should allow a hook named in enabled', () => { expect(HookFilter.convertIgnoredHooksToEnabledHooks([], [], DEFAULT_SET)).to.have.members(DEFAULT_SET);
expect(filter.shouldSkip('enabled-hook')).to.be.false; });
});
it('should skip a hook named in defaults but also in ignored', () => { it('should correctly include default and enabled hooks when ignored hooks is set', () => {
expect(filter.shouldSkip('default-allowed-but-ignored')).to.be.true; expect(HookFilter.convertIgnoredHooksToEnabledHooks(ENABLED_SET, ['my-ignored-hook'], DEFAULT_SET)).to.have.members([
}); ...ENABLED_SET, ...DEFAULT_SET
it('should skip a hook named in enabled but also in ignored', () => { ]);
expect(filter.shouldSkip('enabled-but-ignored')).to.be.true; });
});
it('should skip if any hooks are in ignored', () => { it('should deduplicate', () => {
expect(filter.shouldSkip('enabled-hook', 'enabled-but-ignored')).to.be.true; expect(HookFilter.convertIgnoredHooksToEnabledHooks(DEFAULT_SET, [], DEFAULT_SET)).to.have.members(DEFAULT_SET);
});
it('should correctly exclude ignored hooks', () => {
expect(HookFilter.convertIgnoredHooksToEnabledHooks([], [DEFAULT_SET[0]], DEFAULT_SET)).to.not.include([
DEFAULT_SET[0]
]);
});
it('should handle ignored root hooks', () => {
const defaultHooks = ['myhook', 'myhook.foo', 'myhook.foo.bar'];
expect(HookFilter.convertIgnoredHooksToEnabledHooks([], ['myhook.foo.bar'], defaultHooks)).to.have.members([
'myhook', 'myhook.foo'
]);
expect(HookFilter.convertIgnoredHooksToEnabledHooks([], ['myhook.foo'], defaultHooks)).to.have.members([
'myhook'
]);
expect(HookFilter.convertIgnoredHooksToEnabledHooks([], ['myhook'], defaultHooks)).to.be.empty;
});
}); });
}); });

View File

@ -5,6 +5,7 @@ import { UserTokenStore } from "../../src/UserTokenStore";
import { DefaultConfig } from "../../src/Config/Defaults"; import { DefaultConfig } from "../../src/Config/Defaults";
import { AppserviceMock } from "../utils/AppserviceMock"; import { AppserviceMock } from "../utils/AppserviceMock";
import { ApiError, ErrCode, ValidatorApiError } from "../../src/api"; import { ApiError, ErrCode, ValidatorApiError } from "../../src/api";
import { expect } from "chai";
const ROOM_ID = "!foo:bar"; const ROOM_ID = "!foo:bar";
@ -64,7 +65,7 @@ describe("GitHubRepoConnection", () => {
GitHubRepoConnection.validateState({ GitHubRepoConnection.validateState({
org: "foo", org: "foo",
repo: "bar", repo: "bar",
ignoreHooks: ["issue", "pull_request", "release"], enableHooks: ["issue", "pull_request", "release"],
commandPrefix: "!foo", commandPrefix: "!foo",
showIssueRoomLink: true, showIssueRoomLink: true,
prDiff: { prDiff: {
@ -81,6 +82,16 @@ describe("GitHubRepoConnection", () => {
} }
} as GitHubRepoConnectionState as unknown as Record<string, unknown>); } as GitHubRepoConnectionState as unknown as Record<string, unknown>);
}); });
it("will convert ignoredHooks for existing state", () => {
const state = GitHubRepoConnection.validateState({
org: "foo",
repo: "bar",
ignoreHooks: ["issue"],
enableHooks: ["issue", "pull_request", "release"],
commandPrefix: "!foo",
} as GitHubRepoConnectionState as unknown as Record<string, unknown>, true);
expect(state.enableHooks).to.not.contain('issue');
});
it("will disallow invalid state", () => { it("will disallow invalid state", () => {
try { try {
GitHubRepoConnection.validateState({ GitHubRepoConnection.validateState({
@ -93,12 +104,12 @@ describe("GitHubRepoConnection", () => {
} }
} }
}); });
it("will disallow ignoreHooks to contains invalid enums if this is new state", () => { it("will disallow enabledHooks to contains invalid enums if this is new state", () => {
try { try {
GitHubRepoConnection.validateState({ GitHubRepoConnection.validateState({
org: "foo", org: "foo",
repo: "bar", repo: "bar",
ignoreHooks: ["issue", "pull_request", "release", "not-real"], enabledHooks: ["not-real"],
}, false); }, false);
} catch (ex) { } catch (ex) {
if (ex instanceof ApiError === false || ex.errcode !== ErrCode.BadValue) { if (ex instanceof ApiError === false || ex.errcode !== ErrCode.BadValue) {
@ -106,11 +117,11 @@ describe("GitHubRepoConnection", () => {
} }
} }
}); });
it("will allow ignoreHooks to contains invalid enums if this is old state", () => { it("will allow enabledHooks to contains invalid enums if this is old state", () => {
GitHubRepoConnection.validateState({ GitHubRepoConnection.validateState({
org: "foo", org: "foo",
repo: "bar", repo: "bar",
ignoreHooks: ["issue", "pull_request", "release", "not-real"], enabledHooks: ["not-real"],
}, true); }, true);
}); });
}); });

View File

@ -3,6 +3,7 @@ import { UserTokenStore } from "../../src/UserTokenStore";
import { AppserviceMock } from "../utils/AppserviceMock"; import { AppserviceMock } from "../utils/AppserviceMock";
import { ApiError, ErrCode, ValidatorApiError } from "../../src/api"; import { ApiError, ErrCode, ValidatorApiError } from "../../src/api";
import { GitLabRepoConnection, GitLabRepoConnectionState } from "../../src/Connections"; import { GitLabRepoConnection, GitLabRepoConnectionState } from "../../src/Connections";
import { expect } from "chai";
const ROOM_ID = "!foo:bar"; const ROOM_ID = "!foo:bar";
@ -60,7 +61,7 @@ describe("GitLabRepoConnection", () => {
GitLabRepoConnection.validateState({ GitLabRepoConnection.validateState({
instance: "foo", instance: "foo",
path: "bar/baz", path: "bar/baz",
ignoreHooks: [ enableHooks: [
"merge_request.open", "merge_request.open",
"merge_request.close", "merge_request.close",
"merge_request.merge", "merge_request.merge",
@ -79,6 +80,17 @@ describe("GitLabRepoConnection", () => {
excludingLabels: ["but-not-me"], excludingLabels: ["but-not-me"],
} as GitLabRepoConnectionState as unknown as Record<string, unknown>); } as GitLabRepoConnectionState as unknown as Record<string, unknown>);
}); });
it("will convert ignoredHooks for existing state", () => {
const state = GitLabRepoConnection.validateState({
instance: "foo",
path: "bar/baz",
ignoreHooks: [
"merge_request",
],
commandPrefix: "!gl",
} as GitLabRepoConnectionState as unknown as Record<string, unknown>, true);
expect(state.enableHooks).to.not.contain('merge_request');
});
it("will disallow invalid state", () => { it("will disallow invalid state", () => {
try { try {
GitLabRepoConnection.validateState({ GitLabRepoConnection.validateState({
@ -91,12 +103,12 @@ describe("GitLabRepoConnection", () => {
} }
} }
}); });
it("will disallow ignoreHooks to contains invalid enums if this is new state", () => { it("will disallow enabledHooks to contains invalid enums if this is new state", () => {
try { try {
GitLabRepoConnection.validateState({ GitLabRepoConnection.validateState({
instance: "bar", instance: "bar",
path: "foo", path: "foo",
ignoreHooks: ["issue", "pull_request", "release", "not-real"], enabledHooks: ["not-real"],
}, false); }, false);
} catch (ex) { } catch (ex) {
if (ex instanceof ApiError === false || ex.errcode !== ErrCode.BadValue) { if (ex instanceof ApiError === false || ex.errcode !== ErrCode.BadValue) {
@ -104,11 +116,11 @@ describe("GitLabRepoConnection", () => {
} }
} }
}); });
it("will allow ignoreHooks to contains invalid enums if this is old state", () => { it("will allow enabledHooks to contains invalid enums if this is old state", () => {
GitLabRepoConnection.validateState({ GitLabRepoConnection.validateState({
instance: "bar", instance: "bar",
path: "foo", path: "foo",
ignoreHooks: ["issues", "merge_request", "foo"], enabledHooks: ["not-real"],
}, true); }, true);
}); });
}); });

View File

@ -1,5 +1,5 @@
/* eslint-disable no-console */ /* eslint-disable no-console */
import { h, Component } from 'preact'; import { Component } from 'preact';
import WA, { MatrixCapabilities } from 'matrix-widget-api'; import WA, { MatrixCapabilities } from 'matrix-widget-api';
import { BridgeAPI, BridgeAPIError } from './BridgeAPI'; import { BridgeAPI, BridgeAPIError } from './BridgeAPI';
import { BridgeRoomState } from '../src/Widgets/BridgeWidgetInterface'; import { BridgeRoomState } from '../src/Widgets/BridgeWidgetInterface';

View File

@ -1,4 +1,3 @@
import { h } from "preact";
import { useEffect, useState, useCallback } from 'preact/hooks'; import { useEffect, useState, useCallback } from 'preact/hooks';
import { BridgeRoomState } from "../../src/Widgets/BridgeWidgetInterface"; import { BridgeRoomState } from "../../src/Widgets/BridgeWidgetInterface";
import GeneralConfig from './configs/GeneralConfig'; import GeneralConfig from './configs/GeneralConfig';

View File

@ -1,4 +1,3 @@
import { h } from "preact";
import style from "./ConnectionCard.module.scss"; import style from "./ConnectionCard.module.scss";
interface IProps { interface IProps {

View File

@ -1,4 +1,4 @@
import { h, FunctionComponent } from 'preact'; import { FunctionComponent } from 'preact';
import { BridgeRoomStateGitHub } from '../../src/Widgets/BridgeWidgetInterface'; import { BridgeRoomStateGitHub } from '../../src/Widgets/BridgeWidgetInterface';
import "./GitHubState.css"; import "./GitHubState.css";

View File

@ -1,4 +1,4 @@
import { h, FunctionComponent } from "preact"; import { FunctionComponent } from "preact";
import style from "./ServiceCard.module.scss"; import style from "./ServiceCard.module.scss";

View File

@ -1,4 +1,3 @@
import { h } from "preact";
import { Button } from "../elements"; import { Button } from "../elements";
export default function GeneralConfig() { export default function GeneralConfig() {

View File

@ -1,4 +1,4 @@
import { h, FunctionComponent } from "preact"; import { FunctionComponent } from "preact";
import ErrorBadge from "../../icons/error-badge.svg"; import ErrorBadge from "../../icons/error-badge.svg";
import style from "./ErrorPane.module.scss"; import style from "./ErrorPane.module.scss";

View File

@ -0,0 +1,22 @@
import { FunctionComponent } from "preact";
import { JSXInternal } from "preact/src/jsx";
export const EventHookCheckbox: FunctionComponent<{
enabledHooks: string[],
onChange: JSXInternal.GenericEventHandler<HTMLInputElement>,
hookEventName: string,
parentEvent?: string,
}> = ({enabledHooks, onChange, hookEventName, parentEvent, children}) => {
const checked = enabledHooks.includes(hookEventName) || (!!parentEvent && enabledHooks.includes(parentEvent));
return <li>
<label>
<input
type="checkbox"
x-event-name={hookEventName}
checked={checked}
onChange={onChange} />
{ children }
</label>
</li>;
};

View File

@ -1,4 +1,4 @@
import { h, FunctionComponent } from "preact"; import { FunctionComponent } from "preact";
import style from "./InputField.module.scss"; import style from "./InputField.module.scss";
interface Props { interface Props {

View File

@ -1,4 +1,4 @@
import { h, FunctionComponent } from "preact" import { FunctionComponent } from "preact"
import { useState } from "preact/hooks" import { useState } from "preact/hooks"
import style from "./ListItem.module.scss"; import style from "./ListItem.module.scss";

View File

@ -1,4 +1,4 @@
import { h, FunctionComponent } from "preact"; import { FunctionComponent } from "preact";
import WarningBadge from "../../icons/warning-badge.svg"; import WarningBadge from "../../icons/warning-badge.svg";
import style from "./WarningPane.module.scss"; import style from "./WarningPane.module.scss";

View File

@ -1,4 +1,4 @@
import { h, FunctionComponent, createRef } from "preact"; import { FunctionComponent, createRef } from "preact";
import { useCallback } from "preact/hooks" import { useCallback } from "preact/hooks"
import { BridgeConfig } from "../../BridgeAPI"; import { BridgeConfig } from "../../BridgeAPI";
import { FeedConnectionState, FeedResponseItem } from "../../../src/Connections/FeedConnection"; import { FeedConnectionState, FeedResponseItem } from "../../../src/Connections/FeedConnection";

View File

@ -1,4 +1,4 @@
import { h, FunctionComponent, createRef } from "preact"; import { FunctionComponent, createRef } from "preact";
import { useCallback, useState } from "preact/hooks" import { useCallback, useState } from "preact/hooks"
import CodeMirror from '@uiw/react-codemirror'; import CodeMirror from '@uiw/react-codemirror';
import { javascript } from '@codemirror/lang-javascript'; import { javascript } from '@codemirror/lang-javascript';

View File

@ -1,11 +1,12 @@
import { h, FunctionComponent, createRef } from "preact"; import GitHubIcon from "../../icons/github.png";
import { useState, useCallback, useEffect, useMemo } from "preact/hooks";
import { BridgeAPI, BridgeConfig } from "../../BridgeAPI"; import { BridgeAPI, BridgeConfig } from "../../BridgeAPI";
import { ConnectionConfigurationProps, RoomConfig } from "./RoomConfig"; import { ConnectionConfigurationProps, RoomConfig } from "./RoomConfig";
import { ErrCode } from "../../../src/api"; import { ErrCode } from "../../../src/api";
import { EventHookCheckbox } from '../elements/EventHookCheckbox';
import { FunctionComponent, createRef } from "preact";
import { GitHubRepoConnectionState, GitHubRepoResponseItem, GitHubRepoConnectionRepoTarget, GitHubTargetFilter, GitHubRepoConnectionOrgTarget } from "../../../src/Connections/GithubRepo"; import { GitHubRepoConnectionState, GitHubRepoResponseItem, GitHubRepoConnectionRepoTarget, GitHubTargetFilter, GitHubRepoConnectionOrgTarget } from "../../../src/Connections/GithubRepo";
import { InputField, ButtonSet, Button, ErrorPane } from "../elements"; import { InputField, ButtonSet, Button, ErrorPane } from "../elements";
import GitHubIcon from "../../icons/github.png"; import { useState, useCallback, useEffect, useMemo } from "preact/hooks";
const EventType = "uk.half-shot.matrix-hookshot.github.repository"; const EventType = "uk.half-shot.matrix-hookshot.github.repository";
const NUM_REPOS_PER_PAGE = 10; const NUM_REPOS_PER_PAGE = 10;
@ -119,76 +120,15 @@ const ConnectionSearch: FunctionComponent<{api: BridgeAPI, onPicked: (state: Git
</div>; </div>;
} }
const EventCheckbox: FunctionComponent<{
ignoredHooks?: string[],
enabledHooks?: string[],
onChange: (evt: HTMLInputElement) => void,
eventName: string,
parentEvent?: string,
}> = ({ignoredHooks, enabledHooks, onChange, eventName, parentEvent, children}) => {
let disabled = false;
let checked = false;
if (!enabledHooks && !ignoredHooks) {
throw Error(`Invalid configuration for checkbox ${eventName}`);
}
if (enabledHooks) {
disabled = !!(parentEvent && !enabledHooks.includes(parentEvent));
checked = enabledHooks.includes(eventName);
if (ignoredHooks?.includes(eventName)) {
// If both are set, this was previously a on-by-default event
// that is now off-by-default, and so we need to check both fields.
disabled = !!(parentEvent && ignoredHooks.includes(parentEvent));
checked = true
}
} else if (ignoredHooks) {
// If enabled hooks is not set, this is on-by-default hook.
disabled = !!(parentEvent && ignoredHooks.includes(parentEvent));
checked = !ignoredHooks.includes(eventName);
}
return <li>
<label>
<input
disabled={disabled}
type="checkbox"
x-event-name={eventName}
checked={checked}
onChange={onChange} />
{ children }
</label>
</li>;
};
const ConnectionConfiguration: FunctionComponent<ConnectionConfigurationProps<never, GitHubRepoResponseItem, GitHubRepoConnectionState>> = ({api, existingConnection, onSave, onRemove }) => { const ConnectionConfiguration: FunctionComponent<ConnectionConfigurationProps<never, GitHubRepoResponseItem, GitHubRepoConnectionState>> = ({api, existingConnection, onSave, onRemove }) => {
const [ignoredHooks, setIgnoredHooks] = useState<string[]>(existingConnection?.config.ignoreHooks || []);
// Only used for off-by-default hooks.
const [enabledHooks, setEnabledHooks] = useState<string[]>(existingConnection?.config.enableHooks || []); const [enabledHooks, setEnabledHooks] = useState<string[]>(existingConnection?.config.enableHooks || []);
const toggleIgnoredHook = useCallback(evt => { const toggleEnabledHook = useCallback((evt: any) => {
const key = (evt.target as HTMLElement).getAttribute('x-event-name');
if (key) {
setIgnoredHooks(ignoredHooks => (
ignoredHooks.includes(key) ? ignoredHooks.filter(k => k !== key) : [...ignoredHooks, key]
));
// Remove from enabledHooks
setEnabledHooks(enabledHooks => (
enabledHooks.filter(k => k !== key)
));
}
}, []);
const toggleEnabledHook = useCallback(evt => {
const key = (evt.target as HTMLElement).getAttribute('x-event-name'); const key = (evt.target as HTMLElement).getAttribute('x-event-name');
if (key) { if (key) {
setEnabledHooks(enabledHooks => ( setEnabledHooks(enabledHooks => (
enabledHooks.includes(key) ? enabledHooks.filter(k => k !== key) : [...enabledHooks, key] enabledHooks.includes(key) ? enabledHooks.filter(k => k !== key) : [...enabledHooks, key]
)); ));
// Remove from ignoreHooks
setIgnoredHooks(ignoredHooks => (
ignoredHooks.filter(k => k !== key)
));
} }
}, []); }, []);
@ -205,12 +145,11 @@ const ConnectionConfiguration: FunctionComponent<ConnectionConfigurationProps<ne
if (state) { if (state) {
onSave({ onSave({
...(state), ...(state),
ignoreHooks: ignoredHooks as any[],
enableHooks: enabledHooks as any[], enableHooks: enabledHooks as any[],
commandPrefix: commandPrefixRef.current?.value || commandPrefixRef.current?.placeholder, commandPrefix: commandPrefixRef.current?.value || commandPrefixRef.current?.placeholder,
}); });
} }
}, [enabledHooks, canEdit, existingConnection, connectionState, ignoredHooks, commandPrefixRef, onSave]); }, [enabledHooks, canEdit, existingConnection, connectionState, commandPrefixRef, onSave]);
return <form onSubmit={handleSave}> return <form onSubmit={handleSave}>
{!existingConnection && <ConnectionSearch api={api} onPicked={setConnectionState} />} {!existingConnection && <ConnectionSearch api={api} onPicked={setConnectionState} />}
@ -220,35 +159,35 @@ const ConnectionConfiguration: FunctionComponent<ConnectionConfigurationProps<ne
<InputField visible={!!existingConnection || !!connectionState} label="Events" noPadding={true}> <InputField visible={!!existingConnection || !!connectionState} label="Events" noPadding={true}>
<p>Choose which event should send a notification to the room</p> <p>Choose which event should send a notification to the room</p>
<ul> <ul>
<EventCheckbox ignoredHooks={ignoredHooks} eventName="issue" onChange={toggleIgnoredHook}>Issues</EventCheckbox> <EventHookCheckbox enabledHooks={enabledHooks} hookEventName="issue" onChange={toggleEnabledHook}>Issues</EventHookCheckbox>
<ul> <ul>
<EventCheckbox ignoredHooks={ignoredHooks} parentEvent="issue" eventName="issue.created" onChange={toggleIgnoredHook}>Created</EventCheckbox> <EventHookCheckbox enabledHooks={enabledHooks} parentEvent="issue" hookEventName="issue.created" onChange={toggleEnabledHook}>Created</EventHookCheckbox>
<EventCheckbox ignoredHooks={ignoredHooks} parentEvent="issue" eventName="issue.changed" onChange={toggleIgnoredHook}>Changed</EventCheckbox> <EventHookCheckbox enabledHooks={enabledHooks} parentEvent="issue" hookEventName="issue.changed" onChange={toggleEnabledHook}>Changed</EventHookCheckbox>
<EventCheckbox ignoredHooks={ignoredHooks} parentEvent="issue" eventName="issue.edited" onChange={toggleIgnoredHook}>Edited</EventCheckbox> <EventHookCheckbox enabledHooks={enabledHooks} parentEvent="issue" hookEventName="issue.edited" onChange={toggleEnabledHook}>Edited</EventHookCheckbox>
<EventCheckbox ignoredHooks={ignoredHooks} parentEvent="issue" eventName="issue.labeled" onChange={toggleIgnoredHook}>Labeled</EventCheckbox> <EventHookCheckbox enabledHooks={enabledHooks} parentEvent="issue" hookEventName="issue.labeled" onChange={toggleEnabledHook}>Labeled</EventHookCheckbox>
</ul> </ul>
<EventCheckbox ignoredHooks={ignoredHooks} eventName="pull_request" onChange={toggleIgnoredHook}>Pull requests</EventCheckbox> <EventHookCheckbox enabledHooks={enabledHooks} hookEventName="pull_request" onChange={toggleEnabledHook}>Pull requests</EventHookCheckbox>
<ul> <ul>
<EventCheckbox ignoredHooks={ignoredHooks} parentEvent="pull_request" eventName="pull_request.opened" onChange={toggleIgnoredHook}>Opened</EventCheckbox> <EventHookCheckbox enabledHooks={enabledHooks} parentEvent="pull_request" hookEventName="pull_request.opened" onChange={toggleEnabledHook}>Opened</EventHookCheckbox>
<EventCheckbox ignoredHooks={ignoredHooks} parentEvent="pull_request" eventName="pull_request.closed" onChange={toggleIgnoredHook}>Closed</EventCheckbox> <EventHookCheckbox enabledHooks={enabledHooks} parentEvent="pull_request" hookEventName="pull_request.closed" onChange={toggleEnabledHook}>Closed</EventHookCheckbox>
<EventCheckbox ignoredHooks={ignoredHooks} parentEvent="pull_request" eventName="pull_request.merged" onChange={toggleIgnoredHook}>Merged</EventCheckbox> <EventHookCheckbox enabledHooks={enabledHooks} parentEvent="pull_request" hookEventName="pull_request.merged" onChange={toggleEnabledHook}>Merged</EventHookCheckbox>
<EventCheckbox ignoredHooks={ignoredHooks} parentEvent="pull_request" eventName="pull_request.ready_for_review" onChange={toggleIgnoredHook}>Ready for review</EventCheckbox> <EventHookCheckbox enabledHooks={enabledHooks} parentEvent="pull_request" hookEventName="pull_request.ready_for_review" onChange={toggleEnabledHook}>Ready for review</EventHookCheckbox>
<EventCheckbox ignoredHooks={ignoredHooks} parentEvent="pull_request" eventName="pull_request.reviewed" onChange={toggleIgnoredHook}>Reviewed</EventCheckbox> <EventHookCheckbox enabledHooks={enabledHooks} parentEvent="pull_request" hookEventName="pull_request.reviewed" onChange={toggleEnabledHook}>Reviewed</EventHookCheckbox>
</ul> </ul>
<EventCheckbox enabledHooks={enabledHooks} ignoredHooks={ignoredHooks} eventName="workflow.run" onChange={toggleEnabledHook}>Workflow Runs</EventCheckbox> <EventHookCheckbox enabledHooks={enabledHooks} hookEventName="workflow.run" onChange={toggleEnabledHook}>Workflow Runs</EventHookCheckbox>
<ul> <ul>
<EventCheckbox enabledHooks={enabledHooks} ignoredHooks={ignoredHooks} parentEvent="workflow.run" eventName="workflow.run.success" onChange={toggleEnabledHook}>Success</EventCheckbox> <EventHookCheckbox enabledHooks={enabledHooks} parentEvent="workflow.run" hookEventName="workflow.run.success" onChange={toggleEnabledHook}>Success</EventHookCheckbox>
<EventCheckbox enabledHooks={enabledHooks} ignoredHooks={ignoredHooks} parentEvent="workflow.run" eventName="workflow.run.failure" onChange={toggleEnabledHook}>Failed</EventCheckbox> <EventHookCheckbox enabledHooks={enabledHooks} parentEvent="workflow.run" hookEventName="workflow.run.failure" onChange={toggleEnabledHook}>Failed</EventHookCheckbox>
<EventCheckbox enabledHooks={enabledHooks} ignoredHooks={ignoredHooks} parentEvent="workflow.run" eventName="workflow.run.neutral" onChange={toggleEnabledHook}>Neutral</EventCheckbox> <EventHookCheckbox enabledHooks={enabledHooks} parentEvent="workflow.run" hookEventName="workflow.run.neutral" onChange={toggleEnabledHook}>Neutral</EventHookCheckbox>
<EventCheckbox enabledHooks={enabledHooks} ignoredHooks={ignoredHooks} parentEvent="workflow.run" eventName="workflow.run.cancelled" onChange={toggleEnabledHook}>Cancelled</EventCheckbox> <EventHookCheckbox enabledHooks={enabledHooks} parentEvent="workflow.run" hookEventName="workflow.run.cancelled" onChange={toggleEnabledHook}>Cancelled</EventHookCheckbox>
<EventCheckbox enabledHooks={enabledHooks} ignoredHooks={ignoredHooks} parentEvent="workflow.run" eventName="workflow.run.timed_out" onChange={toggleEnabledHook}>Timed Out</EventCheckbox> <EventHookCheckbox enabledHooks={enabledHooks} parentEvent="workflow.run" hookEventName="workflow.run.timed_out" onChange={toggleEnabledHook}>Timed Out</EventHookCheckbox>
<EventCheckbox enabledHooks={enabledHooks} ignoredHooks={ignoredHooks} parentEvent="workflow.run" eventName="workflow.run.action_required" onChange={toggleEnabledHook}>Action Required</EventCheckbox> <EventHookCheckbox enabledHooks={enabledHooks} parentEvent="workflow.run" hookEventName="workflow.run.action_required" onChange={toggleEnabledHook}>Action Required</EventHookCheckbox>
<EventCheckbox enabledHooks={enabledHooks} ignoredHooks={ignoredHooks} parentEvent="workflow.run" eventName="workflow.run.stale" onChange={toggleEnabledHook}>Stale</EventCheckbox> <EventHookCheckbox enabledHooks={enabledHooks} parentEvent="workflow.run" hookEventName="workflow.run.stale" onChange={toggleEnabledHook}>Stale</EventHookCheckbox>
</ul> </ul>
<EventCheckbox enabledHooks={enabledHooks} ignoredHooks={ignoredHooks} eventName="release" onChange={toggleIgnoredHook}>Releases</EventCheckbox> <EventHookCheckbox enabledHooks={enabledHooks} hookEventName="release" onChange={toggleEnabledHook}>Releases</EventHookCheckbox>
<ul> <ul>
<EventCheckbox enabledHooks={enabledHooks} ignoredHooks={ignoredHooks} parentEvent="release" eventName="release.created" onChange={toggleIgnoredHook}>Published</EventCheckbox> <EventHookCheckbox enabledHooks={enabledHooks} parentEvent="release" hookEventName="release.created" onChange={toggleEnabledHook}>Published</EventHookCheckbox>
<EventCheckbox enabledHooks={enabledHooks} ignoredHooks={ignoredHooks} parentEvent="release" eventName="release.drafted" onChange={toggleEnabledHook}>Drafted</EventCheckbox> <EventHookCheckbox enabledHooks={enabledHooks} parentEvent="release" hookEventName="release.drafted" onChange={toggleEnabledHook}>Drafted</EventHookCheckbox>
</ul> </ul>
</ul> </ul>
</InputField> </InputField>

View File

@ -1,10 +1,11 @@
import { h, FunctionComponent, createRef } from "preact"; import GitLabIcon from "../../icons/gitlab.png";
import { useState, useCallback, useEffect, useMemo } from "preact/hooks";
import { BridgeAPI, BridgeConfig } from "../../BridgeAPI"; import { BridgeAPI, BridgeConfig } from "../../BridgeAPI";
import { ConnectionConfigurationProps, RoomConfig } from "./RoomConfig"; import { ConnectionConfigurationProps, RoomConfig } from "./RoomConfig";
import { EventHookCheckbox } from '../elements/EventHookCheckbox';
import { GitLabRepoConnectionState, GitLabRepoResponseItem, GitLabTargetFilter, GitLabRepoConnectionTarget, GitLabRepoConnectionProjectTarget, GitLabRepoConnectionInstanceTarget } from "../../../src/Connections/GitlabRepo"; import { GitLabRepoConnectionState, GitLabRepoResponseItem, GitLabTargetFilter, GitLabRepoConnectionTarget, GitLabRepoConnectionProjectTarget, GitLabRepoConnectionInstanceTarget } from "../../../src/Connections/GitlabRepo";
import { InputField, ButtonSet, Button, ErrorPane } from "../elements"; import { InputField, ButtonSet, Button, ErrorPane } from "../elements";
import GitLabIcon from "../../icons/gitlab.png"; import { FunctionComponent, createRef } from "preact";
import { useState, useCallback, useEffect, useMemo } from "preact/hooks";
const EventType = "uk.half-shot.matrix-hookshot.gitlab.repository"; const EventType = "uk.half-shot.matrix-hookshot.gitlab.repository";
@ -97,36 +98,18 @@ const ConnectionSearch: FunctionComponent<{api: BridgeAPI, onPicked: (state: Git
</div>; </div>;
} }
const EventCheckbox: FunctionComponent<{
ignoredHooks: string[],
onChange: (evt: HTMLInputElement) => void,
eventName: string,
parentEvent?: string,
}> = ({ignoredHooks, onChange, eventName, parentEvent, children}) => {
return <li>
<label>
<input
disabled={parentEvent && ignoredHooks.includes(parentEvent)}
type="checkbox"
x-event-name={eventName}
checked={!ignoredHooks.includes(eventName)}
onChange={onChange} />
{ children }
</label>
</li>;
};
const ConnectionConfiguration: FunctionComponent<ConnectionConfigurationProps<never, GitLabRepoResponseItem, GitLabRepoConnectionState>> = ({api, existingConnection, onSave, onRemove }) => { const ConnectionConfiguration: FunctionComponent<ConnectionConfigurationProps<never, GitLabRepoResponseItem, GitLabRepoConnectionState>> = ({api, existingConnection, onSave, onRemove }) => {
const [ignoredHooks, setIgnoredHooks] = useState<string[]>(existingConnection?.config.ignoreHooks || []); const [enabledHooks, setEnabledHooks] = useState<string[]>(existingConnection?.config.enableHooks || []);
const toggleIgnoredHook = useCallback(evt => { const toggleEnabledHook = useCallback((evt: any) => {
const key = (evt.target as HTMLElement).getAttribute('x-event-name'); const key = (evt.target as HTMLElement).getAttribute('x-event-name');
if (key) { if (key) {
setIgnoredHooks(ignoredHooks => ( setEnabledHooks(enabledHooks => (
ignoredHooks.includes(key) ? ignoredHooks.filter(k => k !== key) : [...ignoredHooks, key] enabledHooks.includes(key) ? enabledHooks.filter(k => k !== key) : [...enabledHooks, key]
)); ));
} }
}, []); }, []);
const [newInstanceState, setNewInstanceState] = useState<GitLabRepoConnectionState|null>(null); const [newInstanceState, setNewInstanceState] = useState<GitLabRepoConnectionState|null>(null);
const canEdit = !existingConnection || (existingConnection?.canEdit ?? false); const canEdit = !existingConnection || (existingConnection?.canEdit ?? false);
@ -141,12 +124,12 @@ const ConnectionConfiguration: FunctionComponent<ConnectionConfigurationProps<ne
if (state) { if (state) {
onSave({ onSave({
...(state), ...(state),
ignoreHooks: ignoredHooks as any[], enableHooks: enabledHooks as any[],
includeCommentBody: includeBodyRef.current?.checked, includeCommentBody: includeBodyRef.current?.checked,
commandPrefix: commandPrefixRef.current?.value || commandPrefixRef.current?.placeholder, commandPrefix: commandPrefixRef.current?.value || commandPrefixRef.current?.placeholder,
}); });
} }
}, [canEdit, existingConnection, newInstanceState, ignoredHooks, commandPrefixRef, onSave]); }, [includeBodyRef, canEdit, existingConnection, newInstanceState, enabledHooks, commandPrefixRef, onSave]);
return <form onSubmit={handleSave}> return <form onSubmit={handleSave}>
{!existingConnection && <ConnectionSearch api={api} onPicked={setNewInstanceState} />} {!existingConnection && <ConnectionSearch api={api} onPicked={setNewInstanceState} />}
@ -165,18 +148,18 @@ const ConnectionConfiguration: FunctionComponent<ConnectionConfigurationProps<ne
<InputField visible={!!existingConnection || !!newInstanceState} label="Events" noPadding={true}> <InputField visible={!!existingConnection || !!newInstanceState} label="Events" noPadding={true}>
<p>Choose which event should send a notification to the room</p> <p>Choose which event should send a notification to the room</p>
<ul> <ul>
<EventCheckbox ignoredHooks={ignoredHooks} eventName="merge_request" onChange={toggleIgnoredHook}>Merge requests</EventCheckbox> <EventHookCheckbox enabledHooks={enabledHooks} hookEventName="merge_request" onChange={toggleEnabledHook}>Merge requests</EventHookCheckbox>
<ul> <ul>
<EventCheckbox ignoredHooks={ignoredHooks} parentEvent="merge_request" eventName="merge_request.open" onChange={toggleIgnoredHook}>Opened</EventCheckbox> <EventHookCheckbox enabledHooks={enabledHooks} parentEvent="merge_request" hookEventName="merge_request.open" onChange={toggleEnabledHook}>Opened</EventHookCheckbox>
<EventCheckbox ignoredHooks={ignoredHooks} parentEvent="merge_request" eventName="merge_request.close" onChange={toggleIgnoredHook}>Closed</EventCheckbox> <EventHookCheckbox enabledHooks={enabledHooks} parentEvent="merge_request" hookEventName="merge_request.close" onChange={toggleEnabledHook}>Closed</EventHookCheckbox>
<EventCheckbox ignoredHooks={ignoredHooks} parentEvent="merge_request" eventName="merge_request.merge" onChange={toggleIgnoredHook}>Merged</EventCheckbox> <EventHookCheckbox enabledHooks={enabledHooks} parentEvent="merge_request" hookEventName="merge_request.merge" onChange={toggleEnabledHook}>Merged</EventHookCheckbox>
<EventCheckbox ignoredHooks={ignoredHooks} parentEvent="merge_request" eventName="merge_request.review" onChange={toggleIgnoredHook}>Reviewed</EventCheckbox> <EventHookCheckbox enabledHooks={enabledHooks} parentEvent="merge_request" hookEventName="merge_request.review" onChange={toggleEnabledHook}>Reviewed</EventHookCheckbox>
<EventCheckbox ignoredHooks={ignoredHooks} parentEvent="merge_request" eventName="merge_request.ready_for_review" onChange={toggleIgnoredHook}>Ready for review</EventCheckbox> <EventHookCheckbox enabledHooks={enabledHooks} parentEvent="merge_request" hookEventName="merge_request.ready_for_review" onChange={toggleEnabledHook}>Ready for review</EventHookCheckbox>
</ul> </ul>
<EventCheckbox ignoredHooks={ignoredHooks} eventName="push" onChange={toggleIgnoredHook}>Pushes</EventCheckbox> <EventHookCheckbox enabledHooks={enabledHooks} hookEventName="push" onChange={toggleEnabledHook}>Pushes</EventHookCheckbox>
<EventCheckbox ignoredHooks={ignoredHooks} eventName="tag_push" onChange={toggleIgnoredHook}>Tag pushes</EventCheckbox> <EventHookCheckbox enabledHooks={enabledHooks} hookEventName="tag_push" onChange={toggleEnabledHook}>Tag pushes</EventHookCheckbox>
<EventCheckbox ignoredHooks={ignoredHooks} eventName="wiki" onChange={toggleIgnoredHook}>Wiki page updates</EventCheckbox> <EventHookCheckbox enabledHooks={enabledHooks} hookEventName="wiki" onChange={toggleEnabledHook}>Wiki page updates</EventHookCheckbox>
<EventCheckbox ignoredHooks={ignoredHooks} eventName="release" onChange={toggleIgnoredHook}>Releases</EventCheckbox> <EventHookCheckbox enabledHooks={enabledHooks} hookEventName="release" onChange={toggleEnabledHook}>Releases</EventHookCheckbox>
</ul> </ul>
</InputField> </InputField>
<ButtonSet> <ButtonSet>

View File

@ -1,10 +1,11 @@
import { h, FunctionComponent, createRef } from "preact"; import { FunctionComponent, createRef } from "preact";
import { useState, useCallback, useEffect, useMemo } from "preact/hooks"; import { useState, useCallback, useEffect, useMemo } from "preact/hooks";
import { BridgeAPI, BridgeConfig } from "../../BridgeAPI"; import { BridgeAPI, BridgeConfig } from "../../BridgeAPI";
import { ConnectionConfigurationProps, RoomConfig } from "./RoomConfig"; import { ConnectionConfigurationProps, RoomConfig } from "./RoomConfig";
import { ErrCode } from "../../../src/api"; import { ErrCode } from "../../../src/api";
import { JiraProjectConnectionState, JiraProjectResponseItem, JiraProjectConnectionProjectTarget, JiraTargetFilter, JiraProjectConnectionInstanceTarget, JiraProjectConnectionTarget } from "../../../src/Connections/JiraProject"; import { JiraProjectConnectionState, JiraProjectResponseItem, JiraProjectConnectionProjectTarget, JiraTargetFilter, JiraProjectConnectionInstanceTarget, JiraProjectConnectionTarget } from "../../../src/Connections/JiraProject";
import { InputField, ButtonSet, Button, ErrorPane } from "../elements"; import { InputField, ButtonSet, Button, ErrorPane } from "../elements";
import { EventHookCheckbox } from '../elements/EventHookCheckbox';
import JiraIcon from "../../icons/jira.png"; import JiraIcon from "../../icons/jira.png";
const EventType = "uk.half-shot.matrix-hookshot.jira.project"; const EventType = "uk.half-shot.matrix-hookshot.jira.project";
@ -116,23 +117,6 @@ const ConnectionSearch: FunctionComponent<{api: BridgeAPI, onPicked: (state: Jir
</div>; </div>;
} }
const EventCheckbox: FunctionComponent<{
allowedEvents: string[],
onChange: (evt: HTMLInputElement) => void,
eventName: string,
}> = ({allowedEvents, onChange, eventName, children}) => {
return <li>
<label>
<input
type="checkbox"
x-event-name={eventName}
checked={allowedEvents.includes(eventName)}
onChange={onChange} />
{ children }
</label>
</li>;
};
const ConnectionConfiguration: FunctionComponent<ConnectionConfigurationProps<never, JiraProjectResponseItem, JiraProjectConnectionState>> = ({api, existingConnection, onSave, onRemove }) => { const ConnectionConfiguration: FunctionComponent<ConnectionConfigurationProps<never, JiraProjectResponseItem, JiraProjectConnectionState>> = ({api, existingConnection, onSave, onRemove }) => {
const [allowedEvents, setAllowedEvents] = useState<string[]>(existingConnection?.config.events || ['issue_created']); const [allowedEvents, setAllowedEvents] = useState<string[]>(existingConnection?.config.events || ['issue_created']);
@ -173,14 +157,14 @@ const ConnectionConfiguration: FunctionComponent<ConnectionConfigurationProps<ne
<ul> <ul>
Issues Issues
<ul> <ul>
<EventCheckbox allowedEvents={allowedEvents} eventName="issue_created" onChange={toggleEvent}>Created</EventCheckbox> <EventHookCheckbox enabledHooks={allowedEvents} hookEventName="issue_created" onChange={toggleEvent}>Created</EventHookCheckbox>
<EventCheckbox allowedEvents={allowedEvents} eventName="issue_updated" onChange={toggleEvent}>Updated</EventCheckbox> <EventHookCheckbox enabledHooks={allowedEvents} hookEventName="issue_updated" onChange={toggleEvent}>Updated</EventHookCheckbox>
</ul> </ul>
Versions Versions
<ul> <ul>
<EventCheckbox allowedEvents={allowedEvents} eventName="version_created" onChange={toggleEvent}>Created</EventCheckbox> <EventHookCheckbox enabledHooks={allowedEvents} hookEventName="version_created" onChange={toggleEvent}>Created</EventHookCheckbox>
<EventCheckbox allowedEvents={allowedEvents} eventName="version_updated" onChange={toggleEvent}>Updated</EventCheckbox> <EventHookCheckbox enabledHooks={allowedEvents} hookEventName="version_updated" onChange={toggleEvent}>Updated</EventHookCheckbox>
<EventCheckbox allowedEvents={allowedEvents} eventName="version_released" onChange={toggleEvent}>Released</EventCheckbox> <EventHookCheckbox enabledHooks={allowedEvents} hookEventName="version_released" onChange={toggleEvent}>Released</EventHookCheckbox>
</ul> </ul>
</ul> </ul>
</InputField> </InputField>

View File

@ -1,4 +1,4 @@
import { h, FunctionComponent } from "preact"; import { FunctionComponent } from "preact";
import { useCallback, useEffect, useReducer, useState } from "preact/hooks" import { useCallback, useEffect, useReducer, useState } from "preact/hooks"
import { BridgeAPI, BridgeAPIError } from "../../BridgeAPI"; import { BridgeAPI, BridgeAPIError } from "../../BridgeAPI";
import { ErrorPane, ListItem, WarningPane } from "../elements"; import { ErrorPane, ListItem, WarningPane } from "../elements";

View File

@ -1,4 +1,4 @@
import { h, render } from 'preact'; import { render } from 'preact';
import 'preact/devtools'; import 'preact/devtools';
import App from './App'; import App from './App';
import "./fonts/fonts.scss" import "./fonts/fonts.scss"

View File

@ -1,11 +1,12 @@
{ {
"compilerOptions": { "compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "preact",
"module": "commonjs", "module": "commonjs",
"target": "es2019",
"moduleResolution": "node", "moduleResolution": "node",
"jsx": "preserve", "target": "es2019",
"jsxFactory": "h", "types": ["preact"],
/* noEmit - Snowpack builds (emits) files, not tsc. */ /* noEmit - Vite builds (emits) files, not tsc. */
"noEmit": true, "noEmit": true,
/* Additional Options */ /* Additional Options */
"strict": true, "strict": true,