mirror of
https://github.com/langgenius/dify.git
synced 2026-05-31 22:26:19 +08:00
refactor(workflow-generator): scope /create to new-app creation only
The /create command was sniffing window.location.pathname for /app/<id>/
workflow and injecting that into the generator modal as currentAppId, so
"Apply to current draft" would appear when the user fired cmd+k from
inside Studio. The problem: the URL doesn't actually tell us the app's
mode (Workflow vs Advanced-Chat both live under /workflow), so we were
passing the user-picked mode as the "current" mode — which produced a
dead-end if the user picked the wrong one from the submenu.
Refining the current draft is now handled by a dedicated Studio toolbar
button (next commit). /create stays simple: pick workflow / chatflow,
modal opens with no current-app context, the only Apply action is
"Create new app". One responsibility per entry point, zero mode-mismatch
state possible.
Tests updated: the four register-handler cases now assert
openGenerator is called with just { mode } regardless of pathname; the
"nested workflow sub-paths" test is removed (that behaviour is removed
by design).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -67,27 +67,20 @@ describe('/create slash command', () => {
|
||||
createCommand.unregister?.()
|
||||
})
|
||||
|
||||
// Default path: when the user is not in a workflow Studio page, the
|
||||
// handler must open the modal without a currentAppId so the modal only
|
||||
// offers "Create new app".
|
||||
it('should open the generator with no current-app context outside Studio', async () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
writable: true,
|
||||
value: { pathname: '/apps' },
|
||||
})
|
||||
|
||||
// /create is scoped to new-app creation — it MUST always open the modal
|
||||
// with just the requested mode, never with currentAppId. Refining the
|
||||
// current Studio draft is handled by the Studio toolbar button instead.
|
||||
it('should open the generator with only the requested mode (no current-app context)', async () => {
|
||||
await executeCommand('create.open', { mode: 'workflow' })
|
||||
|
||||
expect(mockOpenGenerator).toHaveBeenCalledWith({
|
||||
mode: 'workflow',
|
||||
currentAppId: null,
|
||||
currentAppMode: null,
|
||||
})
|
||||
expect(mockOpenGenerator).toHaveBeenCalledWith({ mode: 'workflow' })
|
||||
})
|
||||
|
||||
// Studio path: the handler must read the app id straight out of the URL
|
||||
// and pass it through so the modal can show "Apply to current draft".
|
||||
it('should capture the current app id when invoked from a workflow Studio URL', async () => {
|
||||
// Critical guarantee: even when invoked from a workflow Studio URL, the
|
||||
// handler must NOT sniff the URL to inject currentAppId — that branch
|
||||
// produced a mode-mismatch dead-end when the user picked the "wrong"
|
||||
// mode from the submenu while inside Studio.
|
||||
it('should NOT capture currentAppId even when invoked from a Studio URL', async () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
writable: true,
|
||||
value: { pathname: '/app/abc-123/workflow' },
|
||||
@ -95,46 +88,16 @@ describe('/create slash command', () => {
|
||||
|
||||
await executeCommand('create.open', { mode: 'advanced-chat' })
|
||||
|
||||
expect(mockOpenGenerator).toHaveBeenCalledWith({
|
||||
mode: 'advanced-chat',
|
||||
currentAppId: 'abc-123',
|
||||
currentAppMode: 'advanced-chat',
|
||||
})
|
||||
expect(mockOpenGenerator).toHaveBeenCalledWith({ mode: 'advanced-chat' })
|
||||
})
|
||||
|
||||
// Defensive fallback: if a caller forgets to pass a mode (or passes none),
|
||||
// the handler must still open the generator with a safe default rather
|
||||
// than crashing the goto-anything dialog.
|
||||
it('should default to workflow mode when no args are passed', async () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
writable: true,
|
||||
value: { pathname: '/apps' },
|
||||
})
|
||||
|
||||
await executeCommand('create.open')
|
||||
|
||||
expect(mockOpenGenerator).toHaveBeenCalledWith({
|
||||
mode: 'workflow',
|
||||
currentAppId: null,
|
||||
currentAppMode: null,
|
||||
})
|
||||
})
|
||||
|
||||
// Studio sub-routes (/app/<id>/workflow/edit, etc.) still need to be
|
||||
// treated as Studio context — the regex must anchor only at the start.
|
||||
it('should still match Studio context for nested workflow sub-paths', async () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
writable: true,
|
||||
value: { pathname: '/app/xyz/workflow/run-history' },
|
||||
})
|
||||
|
||||
await executeCommand('create.open', { mode: 'workflow' })
|
||||
|
||||
expect(mockOpenGenerator).toHaveBeenCalledWith({
|
||||
mode: 'workflow',
|
||||
currentAppId: 'xyz',
|
||||
currentAppMode: 'workflow',
|
||||
})
|
||||
expect(mockOpenGenerator).toHaveBeenCalledWith({ mode: 'workflow' })
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -31,8 +31,16 @@ const OPTIONS: CreateOption[] = [
|
||||
]
|
||||
|
||||
/**
|
||||
* `/create` command — generate a new Workflow or Chatflow app from a natural-
|
||||
* language description. See ``components/workflow/workflow-generator/``.
|
||||
* `/create` command — generate a brand-new Workflow or Chatflow app from a
|
||||
* natural-language description.
|
||||
*
|
||||
* This command is scoped to NEW-app creation only. Refining the current
|
||||
* Studio draft is handled by the toolbar button in
|
||||
* ``components/workflow-app/components/workflow-header/generate-trigger.tsx``,
|
||||
* which opens the same modal with the app's real mode locked + currentAppId
|
||||
* set. Keeping the two journeys separate avoids the mode-mismatch dead-end
|
||||
* the URL-sniffing approach used to produce when /create was triggered from
|
||||
* a Workflow Studio page with the "wrong" mode picked.
|
||||
*/
|
||||
export const createCommand: SlashCommandHandler = {
|
||||
name: 'create',
|
||||
@ -63,25 +71,8 @@ export const createCommand: SlashCommandHandler = {
|
||||
registerCommands({
|
||||
'create.open': async (args) => {
|
||||
const mode: WorkflowGeneratorMode = (args?.mode ?? 'workflow') as WorkflowGeneratorMode
|
||||
// Detect Studio context: /app/<appId>/workflow.
|
||||
let currentAppId: string | null = null
|
||||
let currentAppMode: WorkflowGeneratorMode | null = null
|
||||
if (typeof window !== 'undefined') {
|
||||
const match = window.location.pathname.match(/^\/app\/([^/]+)\/workflow/)
|
||||
if (match) {
|
||||
currentAppId = match[1] ?? null
|
||||
// The /workflow path covers both Workflow and Advanced-Chat apps,
|
||||
// so we don't know the mode for sure from the URL alone. We pass
|
||||
// the requested mode as the "current" mode — the modal compares
|
||||
// them and only enables "Apply to current draft" when they match.
|
||||
currentAppMode = mode
|
||||
}
|
||||
}
|
||||
useWorkflowGeneratorStore.getState().openGenerator({
|
||||
mode,
|
||||
currentAppId,
|
||||
currentAppMode,
|
||||
})
|
||||
// No currentAppId / currentAppMode — /create is new-app only.
|
||||
useWorkflowGeneratorStore.getState().openGenerator({ mode })
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user