Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions app/components/settings/profiles/pr-filter-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
export const PR_FILTER_SCHEMA = {
$schema: 'http://json-schema.org/draft-07/schema#',
title: 'PullRequestFilter',
description: 'Filter configuration for pull request profiles',
type: 'object',
required: ['op', 'filters'],
additionalProperties: false,
properties: {
op: {
type: 'string',
enum: ['AND', 'OR'],
description: 'Logical operator to combine filter nodes',
},
filters: {
type: 'array',
description: 'List of filter nodes to evaluate',
items: {
$ref: '#/definitions/FilterNode',
},
},
},
definitions: {
FilterNode: {
type: 'object',
description: 'A single filter condition',
additionalProperties: false,
properties: {
isDraft: {
type: 'boolean',
description: 'Filter by draft status',
},
author: {
description: 'Filter by PR author',
type: 'object',
required: ['op', 'filters'],
additionalProperties: false,
properties: {
op: { type: 'string', enum: ['AND', 'OR'] },
filters: {
type: 'array',
items: { $ref: '#/definitions/UserFilter' },
},
},
},
reviewers: {
description: 'Filter by PR reviewers',
type: 'object',
required: ['op', 'filters'],
additionalProperties: false,
properties: {
op: { type: 'string', enum: ['AND', 'OR'] },
filters: {
type: 'array',
items: { $ref: '#/definitions/ReviewerFilter' },
},
},
},
targetBranch: {
description: 'Filter by target branch name',
type: 'object',
required: ['op', 'filters'],
additionalProperties: false,
properties: {
op: { type: 'string', enum: ['AND', 'OR'] },
filters: {
type: 'array',
items: { type: 'string' },
},
},
},
sourceBranch: {
description: 'Filter by source branch name',
type: 'object',
required: ['op', 'filters'],
additionalProperties: false,
properties: {
op: { type: 'string', enum: ['AND', 'OR'] },
filters: {
type: 'array',
items: { type: 'string' },
},
},
},
},
},
UserFilter: {
type: 'object',
description: 'Filter for a user',
additionalProperties: false,
properties: {
label: {
type: 'string',
description: 'User display name',
},
id: {
type: 'string',
description: 'User unique identifier',
},
isBot: {
type: 'boolean',
description: 'Is the user a bot',
},
isMySelf: {
type: 'boolean',
description: 'Is the user the current user',
},
},
},
ReviewerFilter: {
type: 'object',
description: 'Filter for a reviewer',
additionalProperties: false,
properties: {
user: {
$ref: '#/definitions/UserFilter',
description: 'Reviewer user filter',
},
isRequired: {
type: 'boolean',
description: 'Is the reviewer required',
},
vote: {
type: 'integer',
enum: [10, 5, 0, -5, -10],
description:
'Reviewer vote: Approved (10), ApprovedWithSuggestions (5), NoVote (0), WaitingForAuthor (-5), Rejected (-10)',
},
},
},
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import Editor, { BeforeMount } from '@monaco-editor/react'
import { useEffect, useState } from 'react'
import { PR_FILTER_SCHEMA } from './pr-filter-schema'

const SCHEMA_URI = 'https://pullrequest-manager.local/profile-filter-schema.json'

const handleBeforeMount: BeforeMount = (monaco) => {
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
validate: true,
allowComments: false,
enableSchemaRequest: false,
schemas: [
{
uri: SCHEMA_URI,
fileMatch: ['**'],
schema: PR_FILTER_SCHEMA,
},
],
})
}

function useMonacoTheme(): string {
const [appTheme, setAppTheme] = useState<string>('system')
const [osDark, setOsDark] = useState<boolean>(
globalThis.matchMedia?.('(prefers-color-scheme: dark)')?.matches ?? false,
)

useEffect(() => {
globalThis.api
.invoke('get-theme')
.then((t: string) => setAppTheme(t))
.catch(() => {})
const unsubTheme = globalThis.api.receive('theme-changed', (t: string) => setAppTheme(t))
const unsubNative = globalThis.api.receive('nativeThemeChanged', () => {
setOsDark(globalThis.matchMedia?.('(prefers-color-scheme: dark)')?.matches ?? false)
})
return () => {
unsubTheme?.()
unsubNative?.()
}
}, [])

const isDark = appTheme === 'dark' || (appTheme === 'system' && osDark)
return isDark ? 'vs-dark' : 'vs'
}

interface Props {
profileId: string
value?: string
onChange: (value: string) => void
}

export default function ProfileFilterEditor({ profileId, value, onChange }: Readonly<Props>) {
const monacoTheme = useMonacoTheme()
const schemaPath = `pullrequest-manager:///profile-filter/${profileId}.json`

return (
<div style={{ flex: 1, minHeight: 0, overflow: 'hidden' }}>
<Editor
height="100%"
language="json"
value={value ?? ''}
theme={monacoTheme}
path={schemaPath}
beforeMount={handleBeforeMount}
onChange={(val) => onChange(val ?? '')}
options={{
minimap: { enabled: false },
scrollBeyondLastLine: false,
wordWrap: 'on',
formatOnPaste: true,
formatOnType: true,
tabSize: 2,
lineNumbers: 'off',
folding: true,
automaticLayout: true,
}}
/>
</div>
)
}
25 changes: 5 additions & 20 deletions app/components/settings/profiles/settings-profiles.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ import {
PopoverSurface,
PopoverTrigger,
Select,
Textarea,
Title1,
tokens,
} from '@fluentui/react-components'
import { AddFilled, BinRecycleRegular, ClipboardPasteFilled, CopyRegular } from '@fluentui/react-icons'
import { useContext, useState } from 'react'
import { useParams } from 'react-router-dom'
import { SettingsContext } from '../context'
import ProfileFilterEditor from './profile-filter-editor.component'

const useStackClassName = makeResetStyles({
display: 'flex',
Expand All @@ -34,17 +34,6 @@ const useStyles = makeStyles({
flex: 1,
height: '100%',
},
height: {
height: '100%',
flexGrow: 1,
display: 'flex',
flexDirection: 'column',
},
textarea: {
maxHeight: 'unset',
height: '100%',
flexGrow: 1,
},
textareaField: {
flexGrow: 1,
display: 'flex',
Expand Down Expand Up @@ -232,19 +221,15 @@ export default function ProfileSettings() {
validationMessage={profile.filterValid ? 'JSON schema is valid.' : 'Invalid schema! Please check your JSON syntax.'}
className={styles.textareaField}
>
<Textarea
className={styles.height}
textarea={{
className: styles.textarea,
}}
name="profiles"
<ProfileFilterEditor
profileId={profile.id}
value={profile.filter}
onChange={(e) => {
onChange={(value) => {
const wrapper = {
profileId: profile.id,
target: {
name: 'filter',
value: e.target.value,
value,
},
}

Expand Down
1 change: 1 addition & 0 deletions app/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/// <reference types="electron-vite/node" />
/// <reference types="vite/client" />

declare module '*.css' {
const content: string
Expand Down
15 changes: 15 additions & 0 deletions app/renderer.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import * as monaco from 'monaco-editor'
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'
import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'
import { loader } from '@monaco-editor/react'
import { WindowContextProvider, menuItems } from '@/lib/window'
import '@/lib/window/window.css'
import appIcon from '@/resources/build/icon.svg'
Expand All @@ -13,6 +17,17 @@ import GeneralSettings from './components/settings/general/general-settings.comp
import ProfileSettings from './components/settings/profiles/settings-profiles.component'
import Settings from './pages/settings/settings.page'

window.MonacoEnvironment = {
getWorker(_, label) {
if (label === 'json') {
return new jsonWorker()
}
return new editorWorker()
},
}

loader.config({ monaco })

ReactDOM.createRoot(document.getElementById('app')!).render(
<React.StrictMode>
<Layout>
Expand Down
Loading