diff --git a/.github/workflows/pr-auto-upsert.yml b/.github/workflows/pr-auto-upsert.yml new file mode 100644 index 00000000..6d9a0c85 --- /dev/null +++ b/.github/workflows/pr-auto-upsert.yml @@ -0,0 +1,65 @@ +name: Auto Upsert Release PR + +on: + # trigger on push to dev branch + push: + branches: + - dev + # trigger on manual workflow_dispatch + workflow_dispatch: + +concurrency: + group: pr-upsert-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: write + pull-requests: write + +jobs: + pr-upsert: + name: Upsert PR + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + ref: "dev" + # fetch all history so that git log can get all commit messages + fetch-depth: 0 + + # create a PR from dev to main, with title in form: Release + # where, is the next version number to be released, based on the last release in git tag + - name: Upsert PR Content + uses: actions/github-script@v6 + with: + github-token: ${{ github.token }} + script: | + const prTitle = "Release to Production" + let body = `> *This PR is automatically created by actions defined in this repository. To see the run log of this action, please click [here](/${{ github.repository }}/actions/runs/${{ github.run_id }})*` + const existedPR = await github.rest.pulls.list({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + head: `${context.repo.owner}:dev`, + base: 'main' + }) + if (existedPR.data.length > 0) { + core.info(`PR already exists: ${existedPR.data[0].html_url}. Updating body...`) + await github.rest.pulls.update({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: existedPR.data[0].number, + body: body + }) + core.info(`PR updated: ${existedPR.data[0].html_url}`) + return + } + const pr = await github.rest.pulls.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: prTitle, + body: body, + head: context.ref, + base: 'main', + draft: true + }) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6f5a309..fa79bd28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## 2025-02-17 + +- 修复了编辑作业时无法修改干员名称的问题 [@Gemini2035](https://github.com/Gemini2035) +- 修复了编辑作业时无法修改动作类型的问题 [@Gemini2035](https://github.com/Gemini2035) +- 添加了是否保存干员过滤器的选项 [@guansss](https://github.com/guansss) +- 使用自定义关卡时支持设置难度 [@guansss](https://github.com/guansss) +- 编辑作业时提醒缺失的干员 [@guansss](https://github.com/guansss) + ## 2025-02-06 - 支持作业集内排序 [@hmydgz](https://github.com/hmydgz) diff --git a/public/assets/operator-avatars/char_4010_etlchi.png b/public/assets/operator-avatars/char_4010_etlchi.png new file mode 100644 index 00000000..4965d328 Binary files /dev/null and b/public/assets/operator-avatars/char_4010_etlchi.png differ diff --git a/public/assets/operator-avatars/char_4173_nowell.png b/public/assets/operator-avatars/char_4173_nowell.png new file mode 100644 index 00000000..50cc29fe Binary files /dev/null and b/public/assets/operator-avatars/char_4173_nowell.png differ diff --git a/src/components/Operations.tsx b/src/components/Operations.tsx index 2a7db30f..04baced3 100644 --- a/src/components/Operations.tsx +++ b/src/components/Operations.tsx @@ -7,7 +7,7 @@ import { } from '@blueprintjs/core' import { UseOperationsParams } from 'apis/operation' -import { useAtom, useAtomValue } from 'jotai' +import { useAtom } from 'jotai' import { debounce } from 'lodash-es' import { ComponentType, useMemo, useState } from 'react' @@ -15,13 +15,9 @@ import { CardTitle } from 'components/CardTitle' import { OperationList } from 'components/OperationList' import { OperationSetList } from 'components/OperationSetList' import { neoLayoutAtom } from 'store/pref' -import { - selectedOperatorQueryAtom, - selectedOperatorsAtom, -} from 'store/selectedOperators' import { authAtom } from '../store/auth' -import { OperatorSelect } from './OperatorSelect' +import { OperatorFilter } from './OperatorFilter' import { withSuspensable } from './Suspensable' export const Operations: ComponentType = withSuspensable(() => { @@ -31,14 +27,13 @@ export const Operations: ComponentType = withSuspensable(() => { limit: 10, orderBy: 'hot', }) - const [selectedOperators, setSelectedOperators] = useAtom( - selectedOperatorsAtom, - ) - const selectedOperatorQuery = useAtomValue(selectedOperatorQueryAtom) const debouncedSetQueryParams = useMemo( () => debounce(setQueryParams, 500), [], ) + + const [selectedOperators, setSelectedOperators] = useState([]) + const [authState] = useAtom(authAtom) const [neoLayout, setNeoLayout] = useAtom(neoLayoutAtom) const [listMode, setListMode] = useState<'operation' | 'operationSet'>( @@ -104,14 +99,7 @@ export const Operations: ComponentType = withSuspensable(() => { {listMode === 'operation' && (
- +
{ } onBlur={() => debouncedSetQueryParams.flush()} /> - - +
{filterNode} @@ -221,7 +209,7 @@ export const Operations: ComponentType = withSuspensable(() => { {listMode === 'operation' && ( diff --git a/src/components/OperatorFilter.tsx b/src/components/OperatorFilter.tsx new file mode 100644 index 00000000..611400ef --- /dev/null +++ b/src/components/OperatorFilter.tsx @@ -0,0 +1,72 @@ +import { Checkbox } from '@blueprintjs/core' + +import { useAtom } from 'jotai' +import { FC, useEffect } from 'react' + +import { + selectedOperatorsAtom, + shouldSaveSelectedOperatorsAtom, +} from '../store/selectedOperators' +import { OperatorSelect } from './OperatorSelect' + +interface OperatorSelectProps { + className?: string + operators: string[] + onChange: (operators: string[]) => void +} + +export const OperatorFilter: FC = ({ + className, + operators, + onChange, +}) => { + const [shouldSaveSelectedOperators, setShouldSaveSelectedOperators] = useAtom( + shouldSaveSelectedOperatorsAtom, + ) + const [savedSelectedOperators, setSavedSelectedOperators] = useAtom( + selectedOperatorsAtom, + ) + + useEffect(() => { + // 用户在另一个标签页中修改选择时,savedSelectedOperators 会同步到当前标签页,需要手动更新给 operators + if (shouldSaveSelectedOperators) { + onChange(savedSelectedOperators) + } + }, [shouldSaveSelectedOperators, savedSelectedOperators, onChange]) + + const handleSelectOperator = (operators: string[]) => { + onChange(operators) + + if (shouldSaveSelectedOperators) { + setSavedSelectedOperators(operators) + } + } + + const handleShouldSaveSelectedOperators = (shouldSave: boolean) => { + setShouldSaveSelectedOperators(shouldSave) + + if (shouldSave) { + setSavedSelectedOperators(operators) + } + } + + return ( +
+ + {operators.length > 0 && ( +
+
点击干员标签以标记为排除该干员
+ + handleShouldSaveSelectedOperators(e.currentTarget.checked) + } + > + 记住选择 + +
+ )} +
+ ) +} diff --git a/src/components/editor/OperationEditor.tsx b/src/components/editor/OperationEditor.tsx index f2237120..12d5a04b 100644 --- a/src/components/editor/OperationEditor.tsx +++ b/src/components/editor/OperationEditor.tsx @@ -196,10 +196,18 @@ const DifficultyPicker: FC<{ const stageName = useWatch({ control, name: 'stageName' }) const { data: levels } = useLevels() - const invalid = useMemo( - () => !hasHardMode(levels, stageName), - [levels, stageName], - ) + const invalid = useMemo(() => { + // if the stageName is a custom level, we always allow setting difficulty + if (!findLevelByStageName(levels, stageName)) { + return false + } + + if (hasHardMode(levels, stageName)) { + return false + } + + return true + }, [levels, stageName]) useEffect(() => { if (invalid && value !== OpDifficulty.UNKNOWN) { diff --git a/src/components/editor/action/EditorActionAdd.tsx b/src/components/editor/action/EditorActionAdd.tsx index 20b2d8bf..0a43fecc 100644 --- a/src/components/editor/action/EditorActionAdd.tsx +++ b/src/components/editor/action/EditorActionAdd.tsx @@ -100,11 +100,15 @@ export const EditorActionAdd = ({ [type], ) + useEffect(() => { + if (editingAction?.type) { + setValue('type', editingAction.type) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [editingAction?._id, setValue]) + useEffect(() => { if (editingAction) { - if ('type' in editingAction) { - setValue('type', editingAction.type) - } // 修复切换type的时候,数据丢失的问题 // 原因:因为切换type的时候会触发页面绘制,导致form和对应的item组件丢失绑定, // 当重置时没办法正常清空item组件内部的值。 diff --git a/src/components/editor/operator/EditorPerformer.tsx b/src/components/editor/operator/EditorPerformer.tsx index 2a15fdf6..f844762e 100644 --- a/src/components/editor/operator/EditorPerformer.tsx +++ b/src/components/editor/operator/EditorPerformer.tsx @@ -1,4 +1,4 @@ -import { NonIdealState } from '@blueprintjs/core' +import { Callout, Icon, NonIdealState } from '@blueprintjs/core' import { Active, DndContext, @@ -17,12 +17,17 @@ import { verticalListSortingStrategy, } from '@dnd-kit/sortable' -import { uniqueId } from 'lodash-es' -import { FC, useEffect, useState } from 'react' -import { Control, UseFieldArrayMove, useFieldArray } from 'react-hook-form' +import { compact, uniq, uniqueId } from 'lodash-es' +import { FC, useEffect, useMemo, useState } from 'react' +import { + Control, + UseFieldArrayMove, + useFieldArray, + useWatch, +} from 'react-hook-form' import { SetRequired } from 'type-fest' -import type { CopilotDocV1 } from 'models/copilot.schema' +import { CopilotDocV1 } from 'models/copilot.schema' import { FactItem } from '../../FactItem' import { Droppable, Sortable } from '../../dnd' @@ -52,6 +57,7 @@ const getId = (performer: Operator | Group) => { export const EditorPerformer: FC = ({ control }) => { const [editMode, setEditMode] = useState('operator') const sensors = useSensors(useSensor(PointerSensor)) + const actions = useWatch({ control, name: 'actions' }) const { fields: _operators, @@ -79,6 +85,25 @@ export const EditorPerformer: FC = ({ control }) => { const operators: Operator[] = _operators const groups: Group[] = _groups + const additionalOperatorsFromActions = useMemo(() => { + if (!actions) return [] + + const additionalOperators = actions.map((action) => { + if ( + 'name' in action && + !operators.some(({ name }) => name === action.name) && + !groups.some(({ opers }) => + opers?.some(({ name }) => name === action.name), + ) + ) { + return action.name + } + return undefined + }) + + return uniq(compact(additionalOperators)) + }, [actions, operators, groups]) + const [draggingOperator, setDraggingOperator] = useState() const [draggingGroup, setDraggingGroup] = useState() const [editingOperator, setEditingOperator] = useState() @@ -289,7 +314,7 @@ export const EditorPerformer: FC = ({ control }) => { addOperator() } else { updateOperator( - operators.findIndex(({ name }) => name === operator.name), + operators.findIndex(({ _id }) => _id === operator._id), operator, ) } @@ -354,14 +379,16 @@ export const EditorPerformer: FC = ({ control }) => { <>
- +
+ +
= ({ control }) => { />
-
+ {additionalOperatorsFromActions.length > 0 && ( + + + 未加入干员:{additionalOperatorsFromActions.join(', ')} + + )} +
{ - const operators = get(selectedOperatorsAtom) - return operators.join(',') -}) +export const shouldSaveSelectedOperatorsAtom = atomWithStorage( + 'maa-copilot-saveSelectedOperators', + true, +)