mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-17 05:29:44 +02:00
feat(editor): Update the Microsoft Teams setup copy and give both flows a Done step (no-changelog) (#41010)
This commit is contained in:
parent
f483ebd9d1
commit
d046047dd0
|
|
@ -306,7 +306,7 @@ describe('TeamsEntraProvisioningService', () => {
|
|||
),
|
||||
});
|
||||
|
||||
await expect(service.provision(options)).rejects.toThrow(/not allowed to register/);
|
||||
await expect(service.provision(options)).rejects.toThrow(/doesn’t let you register apps/);
|
||||
});
|
||||
|
||||
it('refuses a personal Microsoft account', async () => {
|
||||
|
|
@ -434,7 +434,7 @@ describe('TeamsEntraProvisioningService', () => {
|
|||
failed(403, 'Authorization_RequestDenied'),
|
||||
});
|
||||
|
||||
await expect(service.provision(options)).rejects.toThrow(/not allowed to register/);
|
||||
await expect(service.provision(options)).rejects.toThrow(/doesn’t let you register apps/);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -180,10 +180,10 @@ export class TeamsCatalogService {
|
|||
if (!existing) {
|
||||
throw ours
|
||||
? new UserError(
|
||||
'This agent already has an app in the organisation catalogue, and Microsoft has not listed it yet, so it cannot be updated. Nothing is wrong and nothing was lost — try again in a few minutes.',
|
||||
'This agent already has an app in the organization catalog, and Microsoft has not listed it yet, so it cannot be updated. Nothing is wrong and nothing was lost — try again in a few minutes.',
|
||||
)
|
||||
: new UserError(
|
||||
'An app with this name is already in the organisation catalogue, and Microsoft has not listed it yet, so it cannot be updated. If an earlier publish made it, try again in a few minutes. If another app holds the name, rename the agent and try again.',
|
||||
'An app with this name is already in the organization catalog, and Microsoft has not listed it yet, so it cannot be updated. If an earlier publish made it, try again in a few minutes. If another app holds the name, rename the agent and try again.',
|
||||
);
|
||||
}
|
||||
const status = await this.updateExistingApp(token, existing.teamsAppId, archive);
|
||||
|
|
@ -401,13 +401,13 @@ export class TeamsCatalogService {
|
|||
*/
|
||||
private publishError(statusCode: number, body: unknown): Error {
|
||||
if (statusCode === 409 || graphErrorCode(body) === 'Conflict') {
|
||||
return new UserError('An app with this name is already in the organisation catalogue.');
|
||||
return new UserError('An app with this name is already in the organization catalog.');
|
||||
}
|
||||
// Waiting changes nothing about a role the account does not hold, and this
|
||||
// is the answer when even the review route is closed to it.
|
||||
if (this.isPermissionRefusal(statusCode, graphErrorCode(body))) {
|
||||
return new UserError(
|
||||
'This account may not put the app in the organisation catalogue. A Teams administrator has to publish it.',
|
||||
'This account may not put the app in the organization catalog. A Teams administrator has to publish it.',
|
||||
);
|
||||
}
|
||||
return new OperationalError(
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ export class TeamsEntraProvisioningService {
|
|||
// Graph hands the status back rather than throwing, so a refusal would
|
||||
// otherwise read as an empty directory and be blamed on the account type.
|
||||
if (!organization.ok) {
|
||||
throw new OperationalError('Could not read the organisation this account belongs to.');
|
||||
throw new OperationalError('Could not read the organization this account belongs to.');
|
||||
}
|
||||
const first = isRecord(organization.body)
|
||||
? (organization.body.value as unknown[] | undefined)?.[0]
|
||||
|
|
@ -195,7 +195,9 @@ export class TeamsEntraProvisioningService {
|
|||
if (!tenantId) {
|
||||
// A personal Microsoft account has no directory behind it.
|
||||
throw withTeamsFailure(
|
||||
new UserError('Sign in with a work or school account. A personal account cannot be used.'),
|
||||
new UserError(
|
||||
'Teams apps need a work or school account. Sign in again with your organization’s account.',
|
||||
),
|
||||
'personal_account',
|
||||
);
|
||||
}
|
||||
|
|
@ -418,8 +420,8 @@ export class TeamsEntraProvisioningService {
|
|||
throw withTeamsFailure(
|
||||
new UserError(
|
||||
lastCode === 'Authorization_RequestDenied'
|
||||
? 'This organisation does not allow n8n to add a client secret. Use the manual setup.'
|
||||
: 'This organisation refused every client secret lifetime n8n offered. Use the manual setup.',
|
||||
? 'This organization does not allow n8n to add a client secret. Use the manual setup.'
|
||||
: 'This organization refused every client secret lifetime n8n offered. Use the manual setup.',
|
||||
),
|
||||
'secrets_refused',
|
||||
);
|
||||
|
|
@ -433,7 +435,7 @@ export class TeamsEntraProvisioningService {
|
|||
!response.ok &&
|
||||
graphErrorCode(response.body) !== 'Request_MultipleObjectsWithSameKeyValue'
|
||||
) {
|
||||
throw new OperationalError('Could not finish registering the app in this organisation.');
|
||||
throw new OperationalError('Could not finish registering the app in this organization.');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -441,7 +443,7 @@ export class TeamsEntraProvisioningService {
|
|||
if (statusCode === 403 || code === 'Authorization_RequestDenied') {
|
||||
return withTeamsFailure(
|
||||
new UserError(
|
||||
'This account is not allowed to register applications in the organisation. Ask an administrator, or use the manual setup.',
|
||||
'This organization doesn’t let you register apps. Ask an admin to register the app.',
|
||||
),
|
||||
'cannot_register_apps',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -9284,9 +9284,9 @@
|
|||
"agents.channels.discord.setup.interactionsEndpoint.description": "Discord delivers button clicks to this URL. Messages reach your agent over a separate connection, so without it the agent can still chat, but approval buttons will not work.",
|
||||
"agents.channels.discord.setup.interactionsEndpoint.hint": "Save it as the Interactions Endpoint URL on the application's General Information page after the agent is connected and published.",
|
||||
"agents.channels.setup.publishNotice": "Publish this agent before adding the Interactions Endpoint URL in Discord.",
|
||||
"agents.channels.teams.messagingEndpointUrl.label": "Messaging endpoint URL",
|
||||
"agents.channels.teams.messagingEndpointUrl.label": "Messaging endpoint",
|
||||
"agents.channels.teams.setup.createBot.title": "Deploy Azure Bot",
|
||||
"agents.channels.teams.setup.createBot.description": "Prefilled from your credential. You choose the subscription and resource group.",
|
||||
"agents.channels.teams.setup.createBot.description": "Prefilled from your credential; you choose the subscription and resource group.",
|
||||
"agents.channels.teams.setup.createBot.button": "Deploy to Azure",
|
||||
"agents.channels.teams.setup.createBot.hint": "Needs an Azure subscription. Opens Azure with the n8n bot template filled in (Azure may note it isn’t from Microsoft).",
|
||||
"agents.channels.teams.setup.availability.title": "Choose where it works",
|
||||
|
|
@ -9296,14 +9296,14 @@
|
|||
"agents.channels.teams.setup.availability.teamChannels": "Team channels",
|
||||
"agents.channels.teams.setup.availability.teamChannelsHint": "Members can add and {'@'}mention the agent in a channel",
|
||||
"agents.channels.teams.setup.availability.groupChats": "Group and meeting chats",
|
||||
"agents.channels.teams.setup.availability.groupChatsHint": "Anyone in the chat can add the agent and message it",
|
||||
"agents.channels.teams.setup.availability.groupChatsHint": "Anyone in the chat can add the agent and {'@'}mention it",
|
||||
"agents.channels.teams.setup.availability.readAllChannelMessages": "Read all channel messages",
|
||||
"agents.channels.teams.setup.availability.readAllChannelMessagesHint": "The agent runs on every channel message and decides whether to reply, which uses model credits each time",
|
||||
"agents.channels.teams.setup.availability.readAllChannelMessagesHint": "Team owners approve this when they add the app",
|
||||
"agents.channels.teams.setup.availability.readAllGroupMessages": "Read all group-chat messages",
|
||||
"agents.channels.teams.setup.availability.readAllGroupMessagesHint": "The agent runs on every chat message and decides whether to reply, which uses model credits each time",
|
||||
"agents.channels.teams.setup.availability.readAllGroupMessagesHint": "Approved by the person who adds the app to the chat",
|
||||
"agents.channels.teams.setup.registerApp.title": "Register application",
|
||||
"agents.channels.teams.setup.registerApp.description": "Go to Microsoft Entra and register a new application. Skip this step if you already have a credential.",
|
||||
"agents.channels.teams.setup.registerApp.hint": "Copy the Directory (tenant) ID and Application (client) ID. Then add a client secret and copy its value.",
|
||||
"agents.channels.teams.setup.registerApp.hint": "Copy the Directory (tenant) ID and Application (client) ID, then add a client secret and copy its value.",
|
||||
"agents.channels.teams.setup.registerApp.button": "Open Microsoft Entra",
|
||||
"agents.channels.teams.setup.availability.readsChannels": "Reads channels",
|
||||
"agents.channels.teams.setup.availability.readsGroupChats": "Reads group chats",
|
||||
|
|
@ -9312,7 +9312,7 @@
|
|||
"agents.channels.teams.setup.availability.fileAccessNotApproved": "The agent can’t read files in channels and group chats until an admin approves.",
|
||||
"agents.channels.teams.setup.availability.fileAccessApprovedHint": "It can take up to an hour to apply.",
|
||||
"agents.channels.teams.setup.availability.summarySeparator": " · ",
|
||||
"agents.channels.teams.setup.install.identityTooltip": "Name and avatar come from the agent. Teams needs a description, so this default is used. To change any of it, edit the agent and download the package again.",
|
||||
"agents.channels.teams.setup.install.identityTooltip": "Name and avatar come from the agent. To change them, edit the agent and download the package again.",
|
||||
"agents.channels.teams.setup.install.downloaded": "App package downloaded. Upload it in Teams under Apps › Manage your apps › Upload an app.",
|
||||
"agents.channels.teams.setup.createBot.needsCredential": "Pick a verified credential first. The deployment uses its Application and Directory IDs.",
|
||||
"agents.channels.teams.setup.createBot.needsFreeCredential": "Pick a credential that no other agent uses before you deploy.",
|
||||
|
|
@ -9322,7 +9322,6 @@
|
|||
"agents.channels.teams.setup.install.staleDownload": "The credential changed during the download, so that package is for the previous bot. Download it again.",
|
||||
"agents.channels.teams.setup.install.title": "Connect Microsoft Teams",
|
||||
"agents.channels.teams.setup.download.title": "Download app package",
|
||||
"agents.channels.teams.setup.download.description": "Built from the steps above.",
|
||||
"agents.channels.teams.setup.install.description": "Upload the zip in Teams under Apps › Manage your apps › Upload an app.",
|
||||
"agents.channels.teams.setup.install.descriptionWithScope": "{upload} {addTo}",
|
||||
"agents.channels.teams.setup.install.addTo.teams": "Then choose Add to a team and pick the team it should join.",
|
||||
|
|
@ -9629,11 +9628,11 @@
|
|||
"agents.channels.teams.setup.install.verified": "Credential verified with Microsoft Entra.",
|
||||
"agents.channels.teams.setup.install.failed.certificate": "This credential uses certificate authentication, which Teams channels cannot use. Switch it to Client Secret.",
|
||||
"agents.channels.teams.setup.install.failed.incomplete": "This credential is missing its tenant ID, client ID or client secret.",
|
||||
"agents.channels.teams.setup.install.failed.rejected": "Microsoft rejected these details. Check the client secret has not expired, and that the tenant and client IDs match the app registration.",
|
||||
"agents.channels.teams.setup.install.failed.rejected": "Doesn’t match an app in Microsoft Entra. Check the IDs, and copy the secret’s Value, not its Secret ID.",
|
||||
"agents.channels.teams.setup.install.failed.unreachable": "Could not reach Microsoft to check the credential. Check this instance has outbound internet access, then try again.",
|
||||
"agents.channels.teams.setup.install.recheck": "Check again",
|
||||
"agents.channels.teams.setup.createCredential.title": "Create credential",
|
||||
"agents.channels.teams.setup.createCredential.description": "Use the tenant ID, client ID and client secret.",
|
||||
"agents.channels.teams.setup.createCredential.description": "Use the Directory (tenant) ID, Application (client) ID and client secret.",
|
||||
"agents.channels.teams.setup.createBot.existingBot": "Use an existing Azure Bot",
|
||||
"agents.channels.teams.setup.createBot.existingBotHint": "Paste this into the bot’s Configuration page and turn on the Microsoft Teams channel.",
|
||||
"agents.channels.teams.setup.availability.whereTitle": "Where can people use the agent?",
|
||||
|
|
@ -9651,27 +9650,23 @@
|
|||
"agents.channels.teams.setup.createCredential.claimed": "Already connected to {agent}. Register a new app for this agent, or disconnect it there first.",
|
||||
"agents.channels.teams.setup.install.failed.cloud": "Teams channels only reach the global Microsoft cloud. Set the credential's Microsoft Graph API base URL back to https://graph.microsoft.com.",
|
||||
"agents.channels.teams.setupKind.recommended": "Recommended setup",
|
||||
"agents.channels.teams.setupKind.manual": "Manual setup",
|
||||
"agents.channels.teams.setupKind.recommendedDescription": "Sign in and we create the app and bot for you",
|
||||
"agents.channels.teams.setupKind.manual": "Use your own Microsoft app",
|
||||
"agents.channels.teams.setupKind.manualDescription": "Connect an app and bot you’ve set up in Azure",
|
||||
"agents.channels.teams.managed.connect.title": "Connect to Microsoft",
|
||||
"agents.channels.teams.managed.connect.description": "Sign in with the work account for the organisation where people will use this agent.",
|
||||
"agents.channels.teams.managed.connect.description": "Sign in with the work account for the organization where people will use this agent.",
|
||||
"agents.channels.teams.managed.connect.lockedBySignIn": "Sign in with Microsoft again to continue.",
|
||||
"agents.channels.teams.managed.connect.button": "Sign in with Microsoft",
|
||||
"agents.channels.teams.managed.connect.hint": "Use a work or school account.",
|
||||
"agents.channels.teams.managed.connect.summary": "Signed in to {organization}",
|
||||
"agents.channels.teams.managed.connect.summaryUnknown": "Signed in to Microsoft",
|
||||
"agents.channels.teams.managed.connect.reconnect": "This sign-in is missing a permission the setup now needs. Sign in again to continue.",
|
||||
"agents.channels.teams.managed.connect.failed": "The Microsoft sign-in did not finish. If it stopped at a permissions screen, an administrator has to approve n8n for your organisation once before anyone can use this setup.",
|
||||
"agents.channels.teams.managed.connect.failed": "The Microsoft sign-in did not finish. If it stopped at a permissions screen, an administrator has to approve n8n for your organization once before anyone can use this setup.",
|
||||
"agents.channels.teams.managed.connect.edit": "Edit this sign-in",
|
||||
"agents.channels.teams.managed.createApp.title": "Create Teams app",
|
||||
"agents.channels.teams.managed.createApp.description": "Registers this agent with Microsoft, so Teams knows who it is. Name and icon come from the agent.",
|
||||
"agents.channels.teams.managed.createApp.locked": "Connect to continue.",
|
||||
"agents.channels.teams.managed.createApp.description": "Registers this agent with Microsoft, so Teams knows who it is.",
|
||||
"agents.channels.teams.managed.createBot.title": "Deploy Azure Bot",
|
||||
"agents.channels.teams.managed.createBot.description": "Creates the bot in your Azure subscription.",
|
||||
"agents.channels.teams.managed.createBot.locked": "Create the app to continue.",
|
||||
"agents.channels.teams.managed.availability.locked": "Deploy the bot to continue.",
|
||||
"agents.channels.teams.managed.availability.description": "Changing these later means publishing or uploading the app again.",
|
||||
"agents.channels.teams.managed.install.title": "Install app",
|
||||
"agents.channels.teams.managed.install.description": "Download the package and upload it in Teams. You can change this later in Channel Settings.",
|
||||
"agents.channels.teams.managed.install.locked": "Choose where it is available to continue.",
|
||||
"agents.channels.teams.managed.install.description": "Add it for yourself, or publish it to everyone. You can change this later in Channel Settings.",
|
||||
"agents.channels.teams.fileAccessConsentFailed": "File access was not approved. Try again, or send the link to an admin who can approve it",
|
||||
"agents.channels.teams.managed.fileAccess.title": "Allow file access",
|
||||
"agents.channels.teams.managed.fileAccess.description": "To read files people attach in channels and group chats, an admin needs to approve file access.",
|
||||
|
|
@ -9685,51 +9680,48 @@
|
|||
"agents.channels.teams.managed.fileAccess.copied": "Link copied",
|
||||
"agents.channels.teams.managed.fileAccess.copiedHint": "Send it to an admin in {organization}",
|
||||
"agents.channels.teams.managed.finish.title": "Connect Microsoft Teams",
|
||||
"agents.channels.teams.managed.finish.description": "Finish adding the app in Teams, then press Done.",
|
||||
"agents.channels.teams.managed.finish.locked": "Install the app first. What to do next depends on how you install it.",
|
||||
"agents.channels.teams.managed.finish.published": "Added to your organisation's app catalog. A Teams administrator makes it available to everyone in the Teams admin centre, under Teams apps › Manage apps.",
|
||||
"agents.channels.teams.managed.finish.locked": "Install the app first.",
|
||||
"agents.channels.teams.managed.finish.published": "Published to the {organization} app catalog. It can take up to a day before people can add it in Teams.",
|
||||
"agents.channels.teams.managed.finish.publishedUnknown": "Published to your organization’s app catalog. It can take up to a day before people can add it in Teams.",
|
||||
"agents.channels.teams.managed.finish.soonerBefore": "Want it sooner for yourself?",
|
||||
"agents.channels.teams.managed.finish.soonerLink": "Download the package",
|
||||
"agents.channels.teams.managed.finish.soonerAfter": "and upload it in Teams.",
|
||||
"agents.channels.teams.managed.useOwnApp": "Use your own Microsoft app instead",
|
||||
"agents.channels.teams.managed.createApp.button": "Create Teams app",
|
||||
"agents.channels.teams.managed.createApp.button": "Create app",
|
||||
"agents.channels.teams.managed.createApp.summary": "Registered in {organization}.",
|
||||
"agents.channels.teams.managed.createApp.viewInEntra": "View in Microsoft Entra",
|
||||
"agents.channels.teams.managed.createBot.subscription": "Azure subscription",
|
||||
"agents.channels.teams.managed.createBot.create": "Create Azure Bot",
|
||||
"agents.channels.teams.managed.createBot.summary": "Bot {botName} is running in Azure",
|
||||
"agents.channels.teams.managed.createBot.noSubscription": "Your Microsoft account does not have access to an Azure subscription, and the bot needs one. Ask your Azure administrator for Contributor access to a subscription, or create the bot yourself in a subscription you can already use.",
|
||||
"agents.channels.teams.managed.createBot.freeTier": "Uses Azure's free bot tier. You can see and delete it in the Azure portal.",
|
||||
"agents.channels.teams.managed.createBot.titleDone": "Azure Bot deployed",
|
||||
"agents.channels.teams.managed.createBot.summary": "Your bot {botName} is running in Azure.",
|
||||
"agents.channels.teams.managed.createBot.noSubscription": "Your Microsoft account doesn’t have access to an Azure subscription. Ask your Azure admin for Contributor access to one, or use",
|
||||
"agents.channels.teams.managed.createBot.noSubscriptionLink": "your own Microsoft app",
|
||||
"agents.channels.teams.managed.createBot.noSubscriptionAfter": ".",
|
||||
"agents.channels.teams.managed.createBot.progress": "Creating Azure Bot, this can take up to a minute…",
|
||||
"agents.channels.teams.managed.createApp.titleDone": "Teams app created",
|
||||
"agents.channels.teams.managed.createApp.progress": "Creating Teams app, this can take up to 30 seconds…",
|
||||
"agents.channels.teams.managed.createBot.noSubscriptionsFound": "No subscriptions found",
|
||||
"agents.channels.teams.managed.install.identityTooltip": "Name and avatar come from the agent. Teams needs a description, so this default is used. To change any of it, edit the agent and update the app in Teams.",
|
||||
"agents.channels.teams.managed.install.publishing": "Publishing to your organisation\u2026",
|
||||
"agents.channels.teams.managed.install.identityTooltip": "Name and avatar come from the agent. To change them, edit the agent and update the app in Teams.",
|
||||
"agents.channels.teams.managed.install.publishing": "Publishing to your organization\u2026",
|
||||
"agents.channels.teams.managed.install.preparing": "Preparing the app package\u2026",
|
||||
"agents.channels.teams.managed.app.submitted": "Waiting for your Teams administrator to approve it. Nobody can add it until they do.",
|
||||
"agents.channels.teams.managed.app.rejected": "Your Teams administrator rejected the app. Microsoft gives no reason here, so ask them in the Teams admin centre.",
|
||||
"agents.channels.teams.managed.app.rejected": "Your Teams administrator rejected the app. Microsoft gives no reason here, so ask them in the Teams admin center.",
|
||||
"agents.channels.teams.managed.install.menu": "Install app",
|
||||
"agents.channels.teams.managed.install.forMe": "Install for me",
|
||||
"agents.channels.teams.managed.install.forMeHint": "Downloads the package to upload in Teams. Only you get it.",
|
||||
"agents.channels.teams.managed.install.publishHintShort": "Goes live for your organisation, or goes to your Teams administrator for approval first.",
|
||||
"agents.channels.teams.managed.install.forMeHint": "Download app package and upload it in Teams.",
|
||||
"agents.channels.teams.managed.install.publishHintShort": "Adds it to the org catalog. A Teams admin may need to approve it first.",
|
||||
"agents.channels.teams.managed.install.downloaded": "Package downloaded. Upload the zip in Teams under Apps \u203a Manage your apps \u203a Upload an app.",
|
||||
"agents.channels.teams.managed.install.downloadAgain": "Download again",
|
||||
"agents.channels.teams.managed.install.noUploadOption": "No Upload an app option? Ask your Teams administrator to allow custom app uploads, or publish instead.",
|
||||
"agents.channels.teams.managed.install.publish": "Publish to organization",
|
||||
"agents.channels.teams.managed.install.submitted": "Sent to your Teams administrator for approval. Nobody can add the app until they approve it.",
|
||||
"agents.channels.teams.managed.install.submittedWhy": "n8n could not publish it outright. A Microsoft administrator approves n8n for the organisation once, and later publishes go live immediately.",
|
||||
"agents.channels.teams.managed.install.submittedWhy": "n8n could not publish it outright. A Microsoft administrator approves n8n for the organization once, and later publishes go live immediately.",
|
||||
"agents.channels.teams.managed.install.copyApprovalRequest": "Copy a request for your administrator",
|
||||
"agents.channels.teams.managed.install.approvalRequestCopied": "Copied — paste it to your administrator",
|
||||
"agents.channels.teams.managed.install.approvalRequest": "Please approve \"{app}\" for Microsoft Teams: {url} — it is waiting under Pending approval.",
|
||||
"agents.channels.teams.managed.install.published": "Published to your organisation.",
|
||||
"agents.channels.teams.managed.install.published": "Published to your organization.",
|
||||
"agents.channels.teams.managed.install.submittedToast": "Sent for approval",
|
||||
"agents.channels.teams.managed.install.publishedToastHint": "A Teams admin will need to make the app available to everyone.",
|
||||
"agents.channels.teams.managed.install.publishedToast": "Published to organization",
|
||||
"agents.channels.teams.managed.install.rejected": "Your Teams administrator rejected the app. Microsoft gives no reason here, so ask them in the Teams admin centre.",
|
||||
"agents.channels.teams.managed.install.rejected": "Your Teams administrator rejected the app. Microsoft gives no reason here, so ask them in the Teams admin center.",
|
||||
"agents.channels.teams.managed.install.unknown": "Microsoft has not listed the app yet. This can take a day or two, so check again later.",
|
||||
"agents.channels.teams.managed.install.donePublished": "Published to your organisation. Microsoft can take up to a day to let people add it.",
|
||||
"agents.channels.teams.managed.install.donePublished": "Published to your organization. Microsoft can take up to a day to let people add it.",
|
||||
"agents.channels.teams.managed.error": "Something went wrong. {message}",
|
||||
"agents.channels.teams.managed.errors.signInFirst": "Sign in with Microsoft first.",
|
||||
"agents.channels.teams.managed.errors.createAppFirst": "Create the Teams app first.",
|
||||
|
|
@ -9741,7 +9733,7 @@
|
|||
"agents.channels.teams.remove.description": "Your agent will be disconnected from Microsoft Teams. Messages already sent will remain in Teams. To reconnect, you'll need to set up Microsoft Teams again.",
|
||||
"agents.channels.teams.remove.confirm": "Yes, remove",
|
||||
"agents.channels.teams.managed.connect.adminConsent": "Sign-in blocked by a permissions screen?",
|
||||
"agents.channels.teams.managed.connect.adminConsentHint": "A Microsoft administrator approves n8n once for the whole organisation. After that, signing in just works.",
|
||||
"agents.channels.teams.managed.connect.adminConsentHint": "A Microsoft administrator approves n8n once for the whole organization. After that, signing in just works.",
|
||||
"agents.channels.teams.managed.createBot.checking": "Looking for an Azure subscription n8n can use…",
|
||||
"agents.channels.teams.managed.createBot.retry": "Check again for an Azure subscription"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -497,13 +497,13 @@ describe('AgentChannelModal', () => {
|
|||
* Done is the only moment the channel can be written. Closing without
|
||||
* writing left it absent from the agent with nothing to show for the run.
|
||||
*/
|
||||
it('writes the channel when the setup is finished from the footer', async () => {
|
||||
it('writes the channel when the view finishes the setup', async () => {
|
||||
platformCanFinish.value = true;
|
||||
selectedCredentials.value.example = 'credential-new';
|
||||
const wrapper = mountModal('example_setup', true);
|
||||
await flushPromises();
|
||||
|
||||
await wrapper.get('[data-testid="agent-channel-setup-done"]').trigger('click');
|
||||
await wrapper.get('[data-testid="platform-done"]').trigger('click');
|
||||
await flushPromises();
|
||||
|
||||
expect(mocks.connect).toHaveBeenCalled();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import type { TeamsCatalogState, TeamsManagedSetupState } from '@n8n/api-types';
|
||||
import type {
|
||||
TeamsAgentSetupState,
|
||||
TeamsCatalogState,
|
||||
TeamsManagedSetupState,
|
||||
} from '@n8n/api-types';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { configure, fireEvent, waitFor, within } from '@testing-library/vue';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
|
@ -150,28 +154,41 @@ describe('AgentChannelTeamsManagedSetup', () => {
|
|||
expect(queryByTestId('teams-admin-consent')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('tells the user what to do first on every step after Connect', () => {
|
||||
const { getByTestId } = render();
|
||||
it('previews the steps after Connect, greyed out, until signed in', async () => {
|
||||
const { getByTestId, getByText, queryByTestId } = render({
|
||||
runtime: buildRuntime({
|
||||
botSetupState: ref<TeamsAgentSetupState | null>({
|
||||
defaultDisplayName: 'Support Agent',
|
||||
defaultDescription: 'Chat with Support Agent, an agent powered by n8n.',
|
||||
} as TeamsAgentSetupState),
|
||||
}),
|
||||
});
|
||||
|
||||
expect(getByTestId('teams-managed-locked-create-app')).toHaveTextContent(
|
||||
'agents.channels.teams.managed.createApp.locked',
|
||||
expect(queryByTestId('teams-create-app')).not.toBeInTheDocument();
|
||||
expect(queryByTestId('teams-install-menu')).not.toBeInTheDocument();
|
||||
for (const id of ['create-app', 'create-bot', 'availability', 'install']) {
|
||||
expect(getByTestId(`teams-managed-preview-${id}`).querySelector('[inert]')).not.toBeNull();
|
||||
}
|
||||
await waitFor(() =>
|
||||
expect(getByTestId('teams-managed-preview-install')).toHaveTextContent('Support Agent'),
|
||||
);
|
||||
expect(getByTestId('teams-managed-locked-create-bot')).toBeVisible();
|
||||
expect(getByTestId('teams-managed-locked-availability')).toBeVisible();
|
||||
expect(getByTestId('teams-managed-locked-install')).toBeVisible();
|
||||
expect(getByTestId('teams-managed-locked-finish')).toBeVisible();
|
||||
expect(
|
||||
within(getByTestId('teams-managed-preview-create-app')).getByRole('button', {
|
||||
name: 'agents.channels.teams.managed.createApp.button',
|
||||
}),
|
||||
).toBeDisabled();
|
||||
expect(getByText('agents.channels.teams.managed.finish.locked')).toBeVisible();
|
||||
expect(getByTestId('teams-managed-done')).toBeDisabled();
|
||||
});
|
||||
|
||||
it('collapses the Connect step to the organisation once signed in', async () => {
|
||||
it('collapses the Connect step to the sign-in once signed in', async () => {
|
||||
const { getByTestId, queryByTestId } = render({
|
||||
modelValue: 'cred-1',
|
||||
setup: setupState({ managerCredentials: [connectedCredential] }),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('teams-manager-organization')).toHaveTextContent(
|
||||
'agents.channels.teams.managed.connect.summary Acme Corp',
|
||||
);
|
||||
expect(getByTestId('teams-manager-credential-edit')).toBeVisible();
|
||||
});
|
||||
expect(queryByTestId('teams-manager-connect')).not.toBeInTheDocument();
|
||||
});
|
||||
|
|
@ -183,10 +200,10 @@ describe('AgentChannelTeamsManagedSetup', () => {
|
|||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(queryByTestId('teams-managed-locked-create-app')).not.toBeInTheDocument();
|
||||
expect(queryByTestId('teams-create-app')).toBeInTheDocument();
|
||||
});
|
||||
// The steps after it stay blocked until their own work is done.
|
||||
expect(queryByTestId('teams-managed-locked-create-bot')).toBeInTheDocument();
|
||||
expect(queryByTestId('teams-create-bot')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('offers a picker when the project already has a sign-in', () => {
|
||||
|
|
@ -673,7 +690,7 @@ describe('AgentChannelTeamsManagedSetup', () => {
|
|||
* user saying they are finished -- not a claim that it worked.
|
||||
*/
|
||||
it('moves the upload instructions to the last step once the package is out', async () => {
|
||||
const { getByTestId, queryByTestId } = render(
|
||||
const { getByTestId, getByText, queryByText, emitted } = render(
|
||||
signedIn({
|
||||
provisionedApp: ref({
|
||||
credentialId: 'bot-cred-1',
|
||||
|
|
@ -693,13 +710,20 @@ describe('AgentChannelTeamsManagedSetup', () => {
|
|||
);
|
||||
|
||||
// Locked until there is something to upload.
|
||||
await waitFor(() => expect(getByTestId('teams-managed-locked-finish')).toBeVisible());
|
||||
expect(queryByTestId('teams-finish-instructions')).not.toBeInTheDocument();
|
||||
await waitFor(() => expect(getByTestId('teams-managed-done')).toBeDisabled());
|
||||
expect(
|
||||
queryByText('agents.channels.teams.setup.install.description'),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
await fireEvent.click(getByTestId('teams-install-download-package'));
|
||||
|
||||
await waitFor(() => expect(getByTestId('teams-finish-instructions')).toBeVisible());
|
||||
expect(getByTestId('teams-finish-upload-blocked')).toBeVisible();
|
||||
await waitFor(() =>
|
||||
expect(getByText('agents.channels.teams.setup.install.description')).toBeVisible(),
|
||||
);
|
||||
expect(getByTestId('teams-finish-upload-hint')).toBeVisible();
|
||||
|
||||
await fireEvent.click(getByTestId('teams-managed-done'));
|
||||
expect(emitted().done).toHaveLength(1);
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
@ -708,7 +732,7 @@ describe('AgentChannelTeamsManagedSetup', () => {
|
|||
* not only in the session that downloaded.
|
||||
*/
|
||||
it('shows the upload instructions again on a setup reopened over a saved channel', async () => {
|
||||
const { getByTestId, queryByTestId } = render({
|
||||
const { getByTestId, getByText } = render({
|
||||
...signedIn({
|
||||
provisionedApp: ref({
|
||||
credentialId: 'bot-cred-1',
|
||||
|
|
@ -728,13 +752,15 @@ describe('AgentChannelTeamsManagedSetup', () => {
|
|||
channelConnected: true,
|
||||
});
|
||||
|
||||
await waitFor(() => expect(getByTestId('teams-finish-instructions')).toBeVisible());
|
||||
expect(queryByTestId('teams-managed-locked-finish')).not.toBeInTheDocument();
|
||||
await waitFor(() =>
|
||||
expect(getByText('agents.channels.teams.setup.install.description')).toBeVisible(),
|
||||
);
|
||||
expect(getByTestId('teams-managed-done')).toBeEnabled();
|
||||
});
|
||||
|
||||
/** A sign-in that has to be redone locks the steps above it, and this one. */
|
||||
it('locks the last step behind a sign-in that has to be redone', async () => {
|
||||
const { getByTestId } = render({
|
||||
const { getByTestId, getAllByText, queryByText } = render({
|
||||
modelValue: 'cred-1',
|
||||
setup: setupState({
|
||||
managerCredentials: [{ ...connectedCredential, reconnectRequired: true }],
|
||||
|
|
@ -742,10 +768,15 @@ describe('AgentChannelTeamsManagedSetup', () => {
|
|||
channelConnected: true,
|
||||
});
|
||||
|
||||
await waitFor(() => expect(getByTestId('teams-managed-locked-finish')).toBeVisible());
|
||||
await waitFor(() => expect(getByTestId('teams-managed-done')).toBeDisabled());
|
||||
// The step says why it is locked once, and not that the app is missing.
|
||||
expect(
|
||||
getAllByText('agents.channels.teams.managed.connect.lockedBySignIn').length,
|
||||
).toBeGreaterThan(0);
|
||||
expect(queryByText('agents.channels.teams.managed.finish.locked')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('warns that an administrator may have blocked custom uploads', async () => {
|
||||
it('says what to do when Teams has no Upload an app option', async () => {
|
||||
const { getByTestId } = render(
|
||||
signedIn({
|
||||
provisionedApp: ref({
|
||||
|
|
@ -770,7 +801,7 @@ describe('AgentChannelTeamsManagedSetup', () => {
|
|||
await waitFor(() => expect(getByTestId('teams-install-download-package')).toBeVisible());
|
||||
await fireEvent.click(getByTestId('teams-install-download-package'));
|
||||
|
||||
await waitFor(() => expect(getByTestId('teams-finish-upload-blocked')).toBeVisible());
|
||||
await waitFor(() => expect(getByTestId('teams-finish-upload-hint')).toBeVisible());
|
||||
});
|
||||
|
||||
/** Nothing else asserts the button actually provisions what was chosen. */
|
||||
|
|
@ -826,8 +857,8 @@ describe('AgentChannelTeamsManagedSetup', () => {
|
|||
|
||||
// The secret's expiry belongs to the connected channel's settings, not here:
|
||||
// at this point it is years away and says nothing about whether setup worked.
|
||||
it('names the app and links to it in Entra once created', async () => {
|
||||
const { getByTestId } = render(
|
||||
it('names the organization the app is registered in once created', async () => {
|
||||
const { getByText, queryByText } = render(
|
||||
signedIn({
|
||||
provisionedApp: ref({
|
||||
credentialId: 'bot-cred-1',
|
||||
|
|
@ -842,20 +873,17 @@ describe('AgentChannelTeamsManagedSetup', () => {
|
|||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(getByTestId('teams-app-summary')).toHaveTextContent(
|
||||
'agents.channels.teams.managed.createApp.summary Acme Corp',
|
||||
),
|
||||
expect(
|
||||
getByText('agents.channels.teams.managed.createApp.summary Acme Corp'),
|
||||
).toBeVisible(),
|
||||
);
|
||||
const link = getByTestId('teams-app-entra-link');
|
||||
expect(link).toHaveAttribute('href', 'https://entra.microsoft.com/app-1');
|
||||
// Carries the design system's own small size, so it matches the line it
|
||||
// sits beside instead of shouting over it.
|
||||
expect(link.tagName).toBe('A');
|
||||
expect(link.className).toMatch(/size-small/);
|
||||
expect(
|
||||
queryByText('agents.channels.teams.managed.createApp.description'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('names the bot once Azure has created it', async () => {
|
||||
const { getByTestId } = render(
|
||||
const { getByText, queryByText } = render(
|
||||
signedIn({
|
||||
provisionedApp: ref({
|
||||
credentialId: 'bot-cred-1',
|
||||
|
|
@ -875,8 +903,13 @@ describe('AgentChannelTeamsManagedSetup', () => {
|
|||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(getByTestId('teams-bot-summary')).toHaveTextContent('support-bot-1'),
|
||||
expect(
|
||||
getByText('agents.channels.teams.managed.createBot.summary support-bot-1'),
|
||||
).toBeVisible(),
|
||||
);
|
||||
expect(
|
||||
queryByText('agents.channels.teams.managed.createBot.description'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('offers the same availability controls as the manual setup', async () => {
|
||||
|
|
@ -1100,7 +1133,7 @@ describe('AgentChannelTeamsManagedSetup', () => {
|
|||
|
||||
it('moves to the last step on a publish, and keeps the package reachable', async () => {
|
||||
const catalogState = ref<TeamsCatalogState | null>({ status: 'unknown', teamsAppId: null });
|
||||
const { getByTestId, queryByTestId } = render(
|
||||
const { getByTestId, getByText, queryByText } = render(
|
||||
signedIn({
|
||||
provisionedApp: ref({
|
||||
credentialId: 'bot-cred-1',
|
||||
|
|
@ -1129,13 +1162,13 @@ describe('AgentChannelTeamsManagedSetup', () => {
|
|||
// The last step says where the app stands, and keeps the package on
|
||||
// offer: a published app is in nobody's Teams until Microsoft lists it.
|
||||
await waitFor(() =>
|
||||
expect(getByTestId('teams-finish-published')).toHaveTextContent(
|
||||
'agents.channels.teams.managed.finish.published',
|
||||
),
|
||||
expect(getByText('agents.channels.teams.managed.finish.published Acme Corp')).toBeVisible(),
|
||||
);
|
||||
expect(getByTestId('teams-finish-download-package')).toBeVisible();
|
||||
// Nothing was added to anyone's Teams, so nothing says it was.
|
||||
expect(queryByTestId('teams-finish-instructions')).not.toBeInTheDocument();
|
||||
expect(
|
||||
queryByText('agents.channels.teams.setup.install.description'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('copies an approval request once the app is in for review', async () => {
|
||||
|
|
|
|||
|
|
@ -36,8 +36,10 @@ import {
|
|||
type TeamsAvailability,
|
||||
} from './constants';
|
||||
import AgentChannelTeamsIdentityCard from './AgentChannelTeamsIdentityCard.vue';
|
||||
import AgentChannelTeamsOptionLabel from './AgentChannelTeamsOptionLabel.vue';
|
||||
import { useTeamsFileAccessMenu } from './useTeamsFileAccessMenu';
|
||||
import type { TeamsChannelRuntime } from './useTeamsChannelRuntime';
|
||||
import { useTeamsUploadInstruction } from './useTeamsUploadInstruction';
|
||||
|
||||
const selectedCredentialId = defineModel<string>({ default: '' });
|
||||
|
||||
|
|
@ -74,6 +76,7 @@ const TEAMS_MANAGE_APPS_URL = 'https://admin.teams.microsoft.com/policies/manage
|
|||
const i18n = useI18n();
|
||||
const clipboard = useClipboard();
|
||||
const rootStore = useRootStore();
|
||||
const uploadInstruction = useTeamsUploadInstruction();
|
||||
|
||||
const busy = ref<string | null>(null);
|
||||
const errorMessage = ref('');
|
||||
|
|
@ -222,19 +225,23 @@ const { items: fileAccessMenuItems, onSelect: onFileAccessMenu } = useTeamsFileA
|
|||
organization,
|
||||
});
|
||||
|
||||
const connectSummary = computed(() =>
|
||||
selectedCredential.value?.organizationName
|
||||
? i18n.baseText('agents.channels.teams.managed.connect.summary', {
|
||||
interpolate: { organization: selectedCredential.value.organizationName },
|
||||
})
|
||||
: i18n.baseText('agents.channels.teams.managed.connect.summaryUnknown'),
|
||||
);
|
||||
|
||||
const currentSettings = computed<AgentTeamsIntegrationSettings>(() => ({
|
||||
...props.savedSettings,
|
||||
...availability.value,
|
||||
}));
|
||||
|
||||
/** True once there is a channel to come back to, so leaving loses nothing. */
|
||||
const persisted = ref(false);
|
||||
|
||||
/**
|
||||
* Leaving is allowed once there is something worth saving: a package handed
|
||||
* over, or an app already in the catalogue. The second case is the one a
|
||||
* re-run lands in -- Microsoft keeps the app through a disconnect, so nothing
|
||||
* here writes the channel again, and without Done there is no way to bind the
|
||||
* credential back to the agent at all.
|
||||
*/
|
||||
const canFinish = computed(() => persisted.value || published.value);
|
||||
|
||||
/**
|
||||
* Every step after the first runs on the sign-in, so a sign-in that has to be
|
||||
* redone locks them rather than leaving one of them looking actionable. A step
|
||||
|
|
@ -256,91 +263,85 @@ const statusOf = computed<Record<string, 'complete' | 'active' | 'locked'>>(() =
|
|||
// itself finished -- the user says when it is. Gated on the sign-in like
|
||||
// the steps above: one that has to be redone locks them, and leaving
|
||||
// this one actionable would read as the odd one out.
|
||||
// Done saves through the modal, which writes only when `canFinish` holds,
|
||||
// so the step unlocks on that and nothing else.
|
||||
finish:
|
||||
connected.value &&
|
||||
(installRoute.value === 'downloaded' || published.value || props.channelConnected === true)
|
||||
connected.value && (canFinish.value || props.channelConnected === true)
|
||||
? ('active' as const)
|
||||
: ('locked' as const),
|
||||
};
|
||||
});
|
||||
|
||||
const steps = computed(() => {
|
||||
const status = statusOf.value;
|
||||
return [
|
||||
{
|
||||
id: 'connect',
|
||||
title: i18n.baseText('agents.channels.teams.managed.connect.title'),
|
||||
description: i18n.baseText('agents.channels.teams.managed.connect.description'),
|
||||
},
|
||||
{
|
||||
id: 'create-app',
|
||||
title: i18n.baseText(
|
||||
status['create-app'] === 'complete'
|
||||
? 'agents.channels.teams.managed.createApp.titleDone'
|
||||
: 'agents.channels.teams.managed.createApp.title',
|
||||
),
|
||||
description: i18n.baseText('agents.channels.teams.managed.createApp.description'),
|
||||
},
|
||||
{
|
||||
id: 'create-bot',
|
||||
title: i18n.baseText(
|
||||
status['create-bot'] === 'complete'
|
||||
? 'agents.channels.teams.managed.createBot.titleDone'
|
||||
: 'agents.channels.teams.managed.createBot.title',
|
||||
),
|
||||
description: i18n.baseText('agents.channels.teams.managed.createBot.description'),
|
||||
},
|
||||
{
|
||||
id: 'availability',
|
||||
title: i18n.baseText('agents.channels.teams.setup.availability.title'),
|
||||
description: i18n.baseText('agents.channels.teams.setup.availability.description'),
|
||||
},
|
||||
{
|
||||
id: 'install',
|
||||
title: i18n.baseText('agents.channels.teams.managed.install.title'),
|
||||
description: i18n.baseText('agents.channels.teams.managed.install.description'),
|
||||
},
|
||||
...(readsSharedConversations.value
|
||||
? [
|
||||
{
|
||||
id: 'file-access',
|
||||
title: i18n.baseText('agents.channels.teams.managed.fileAccess.title'),
|
||||
description: i18n.baseText('agents.channels.teams.managed.fileAccess.description'),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
id: 'finish',
|
||||
title: i18n.baseText('agents.channels.teams.managed.finish.title'),
|
||||
description: i18n.baseText('agents.channels.teams.managed.finish.description'),
|
||||
},
|
||||
];
|
||||
const awaitingCatalog = computed(() => installRoute.value !== 'downloaded' && published.value);
|
||||
|
||||
const finishDescription = computed(() => {
|
||||
if (statusOf.value.finish === 'locked')
|
||||
return i18n.baseText(
|
||||
reconnectRequired.value
|
||||
? 'agents.channels.teams.managed.connect.lockedBySignIn'
|
||||
: 'agents.channels.teams.managed.finish.locked',
|
||||
);
|
||||
if (awaitingCatalog.value) {
|
||||
return organization.value
|
||||
? i18n.baseText('agents.channels.teams.managed.finish.published', {
|
||||
interpolate: { organization: organization.value },
|
||||
})
|
||||
: i18n.baseText('agents.channels.teams.managed.finish.publishedUnknown');
|
||||
}
|
||||
return uploadInstruction(availability.value);
|
||||
});
|
||||
|
||||
// A sign-in that has to be redone blocks every one of them, and the usual
|
||||
// chain would point at a step that is already done. A sign-in not made yet
|
||||
// needs no such help: nothing is done, so the chain reads true.
|
||||
const lockedHints = computed<Record<string, string>>(() => {
|
||||
if (reconnectRequired.value) {
|
||||
const signIn = i18n.baseText('agents.channels.teams.managed.connect.lockedBySignIn');
|
||||
return {
|
||||
'create-app': signIn,
|
||||
'create-bot': signIn,
|
||||
availability: signIn,
|
||||
install: signIn,
|
||||
'file-access': signIn,
|
||||
finish: signIn,
|
||||
};
|
||||
}
|
||||
return {
|
||||
'create-app': i18n.baseText('agents.channels.teams.managed.createApp.locked'),
|
||||
'create-bot': i18n.baseText('agents.channels.teams.managed.createBot.locked'),
|
||||
availability: i18n.baseText('agents.channels.teams.managed.availability.locked'),
|
||||
install: i18n.baseText('agents.channels.teams.managed.install.locked'),
|
||||
'file-access': i18n.baseText('agents.channels.teams.managed.availability.locked'),
|
||||
finish: i18n.baseText('agents.channels.teams.managed.finish.locked'),
|
||||
};
|
||||
});
|
||||
const steps = computed(() => [
|
||||
{
|
||||
id: 'connect',
|
||||
title: i18n.baseText('agents.channels.teams.managed.connect.title'),
|
||||
description: i18n.baseText('agents.channels.teams.managed.connect.description'),
|
||||
},
|
||||
{
|
||||
id: 'create-app',
|
||||
title: i18n.baseText('agents.channels.teams.managed.createApp.title'),
|
||||
description:
|
||||
statusOf.value['create-app'] === 'complete'
|
||||
? i18n.baseText('agents.channels.teams.managed.createApp.summary', {
|
||||
interpolate: { organization: provisionedApp.value?.organizationName ?? '' },
|
||||
})
|
||||
: i18n.baseText('agents.channels.teams.managed.createApp.description'),
|
||||
},
|
||||
{
|
||||
id: 'create-bot',
|
||||
title: i18n.baseText('agents.channels.teams.managed.createBot.title'),
|
||||
description:
|
||||
statusOf.value['create-bot'] === 'complete'
|
||||
? i18n.baseText('agents.channels.teams.managed.createBot.summary', {
|
||||
interpolate: { botName: provisionedBot.value?.botName ?? '' },
|
||||
})
|
||||
: i18n.baseText('agents.channels.teams.managed.createBot.description'),
|
||||
},
|
||||
{
|
||||
id: 'availability',
|
||||
title: i18n.baseText('agents.channels.teams.setup.availability.title'),
|
||||
description: i18n.baseText('agents.channels.teams.managed.availability.description'),
|
||||
},
|
||||
{
|
||||
id: 'install',
|
||||
title: i18n.baseText('agents.channels.teams.managed.install.title'),
|
||||
description: i18n.baseText('agents.channels.teams.managed.install.description'),
|
||||
},
|
||||
...(readsSharedConversations.value
|
||||
? [
|
||||
{
|
||||
id: 'file-access',
|
||||
title: i18n.baseText('agents.channels.teams.managed.fileAccess.title'),
|
||||
description: i18n.baseText('agents.channels.teams.managed.fileAccess.description'),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
id: 'finish',
|
||||
title: i18n.baseText('agents.channels.teams.managed.finish.title'),
|
||||
description: finishDescription.value,
|
||||
},
|
||||
]);
|
||||
|
||||
watch(
|
||||
() => props.setup.managerCredentials,
|
||||
|
|
@ -492,9 +493,6 @@ const provisionBot = async () =>
|
|||
await props.runtime.provisionBot(subscriptionId.value);
|
||||
});
|
||||
|
||||
/** True once there is a channel to come back to, so leaving loses nothing. */
|
||||
const persisted = ref(false);
|
||||
|
||||
/**
|
||||
* Publishes, and stops there. Adding the app to someone's Teams is the step
|
||||
* after, and the design keeps the two apart: publishing is a decision about
|
||||
|
|
@ -539,14 +537,6 @@ watch(
|
|||
|
||||
// Read by the channel modal when it saves, so the availability chosen here is
|
||||
// what gets stored rather than the defaults.
|
||||
/**
|
||||
* Leaving is allowed once there is something worth saving: a package handed
|
||||
* over, or an app already in the catalogue. The second case is the one a
|
||||
* re-run lands in -- Microsoft keeps the app through a disconnect, so nothing
|
||||
* here writes the channel again, and without Done there is no way to bind the
|
||||
* credential back to the agent at all.
|
||||
*/
|
||||
const canFinish = computed(() => persisted.value || published.value);
|
||||
|
||||
defineExpose({ currentSettings, keepOpenAfterConnect: true, canFinish });
|
||||
|
||||
|
|
@ -639,53 +629,6 @@ const downloadPackage = async () =>
|
|||
@click="runtime.editManagerCredential(selectedCredentialId)"
|
||||
/>
|
||||
</div>
|
||||
<N8nText size="small" color="text-light" data-testid="teams-manager-organization">
|
||||
{{ connectSummary }}
|
||||
</N8nText>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="step.id === 'create-app' && statusOf[step.id] === 'complete'"
|
||||
:class="$style.stepContent"
|
||||
>
|
||||
<div :class="$style.summaryLine">
|
||||
<N8nText size="small" color="text-light" data-testid="teams-app-summary">
|
||||
{{
|
||||
i18n.baseText('agents.channels.teams.managed.createApp.summary', {
|
||||
interpolate: { organization: provisionedApp?.organizationName ?? '' },
|
||||
})
|
||||
}}
|
||||
</N8nText>
|
||||
<!--
|
||||
Rendered through N8nText so it carries the same size class as the
|
||||
line beside it. A hand-written font-size drifts from the token the
|
||||
sibling uses, and lost to the anchor styling anyway.
|
||||
-->
|
||||
<N8nText
|
||||
v-if="provisionedApp"
|
||||
tag="a"
|
||||
size="small"
|
||||
:href="provisionedApp.entraAppUrl"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
data-testid="teams-app-entra-link"
|
||||
>
|
||||
{{ i18n.baseText('agents.channels.teams.managed.createApp.viewInEntra') }}
|
||||
</N8nText>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="step.id === 'create-bot' && statusOf[step.id] === 'complete'"
|
||||
:class="$style.stepContent"
|
||||
>
|
||||
<N8nText size="small" color="text-light" data-testid="teams-bot-summary">
|
||||
{{
|
||||
i18n.baseText('agents.channels.teams.managed.createBot.summary', {
|
||||
interpolate: { botName: provisionedBot?.botName ?? '' },
|
||||
})
|
||||
}}
|
||||
</N8nText>
|
||||
</div>
|
||||
|
||||
<div v-else-if="step.id === 'connect'" :class="$style.stepContent">
|
||||
|
|
@ -719,9 +662,6 @@ const downloadPackage = async () =>
|
|||
>
|
||||
{{ i18n.baseText('agents.channels.teams.managed.connect.button') }}
|
||||
</N8nButton>
|
||||
<N8nText size="small" color="text-light">
|
||||
{{ i18n.baseText('agents.channels.teams.managed.connect.hint') }}
|
||||
</N8nText>
|
||||
<!--
|
||||
The one prompt that covers Graph and Azure together. A sign-in cannot:
|
||||
a code is redeemed for one resource at a time.
|
||||
|
|
@ -754,12 +694,11 @@ const downloadPackage = async () =>
|
|||
v-else-if="step.id === 'create-app' && statusOf[step.id] === 'active'"
|
||||
:class="$style.stepContent"
|
||||
>
|
||||
<!-- Offered for the two walls this step can hit: no subscription for
|
||||
the bot that comes next, and a tenant that will not let this account
|
||||
register the app at all. -->
|
||||
<!-- No subscription for the bot that comes next. A tenant that will not
|
||||
let this account register the app links from the error instead. -->
|
||||
<N8nButton
|
||||
v-if="
|
||||
(subscriptionsChecked && subscriptions.length === 0) || errorStepId === 'create-app'
|
||||
subscriptionsChecked && subscriptions.length === 0 && errorStepId !== 'create-app'
|
||||
"
|
||||
variant="ghost"
|
||||
size="small"
|
||||
|
|
@ -836,9 +775,6 @@ const downloadPackage = async () =>
|
|||
/>
|
||||
</N8nSelect>
|
||||
</N8nInputLabel>
|
||||
<N8nText size="small" color="text-light" data-testid="teams-bot-free-tier">
|
||||
{{ i18n.baseText('agents.channels.teams.managed.createBot.freeTier') }}
|
||||
</N8nText>
|
||||
<N8nButton
|
||||
variant="outline"
|
||||
size="medium"
|
||||
|
|
@ -875,15 +811,17 @@ const downloadPackage = async () =>
|
|||
</N8nInputLabel>
|
||||
<N8nText size="small" color="danger" data-testid="teams-bot-no-subscription">
|
||||
{{ i18n.baseText('agents.channels.teams.managed.createBot.noSubscription') }}
|
||||
<N8nLink
|
||||
theme="danger"
|
||||
size="small"
|
||||
underline
|
||||
data-testid="teams-use-own-app-from-bot-step"
|
||||
@click="useOwnMicrosoftAppForBot"
|
||||
>{{
|
||||
i18n.baseText('agents.channels.teams.managed.createBot.noSubscriptionLink')
|
||||
}}</N8nLink
|
||||
>{{ i18n.baseText('agents.channels.teams.managed.createBot.noSubscriptionAfter') }}
|
||||
</N8nText>
|
||||
<N8nButton
|
||||
variant="ghost"
|
||||
size="small"
|
||||
data-testid="teams-use-own-app-from-bot-step"
|
||||
@click="useOwnMicrosoftAppForBot"
|
||||
>
|
||||
{{ i18n.baseText('agents.channels.teams.managed.useOwnApp') }}
|
||||
</N8nButton>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
|
|
@ -948,6 +886,12 @@ const downloadPackage = async () =>
|
|||
:teleported="false"
|
||||
@select="onInstallMenu"
|
||||
>
|
||||
<template #menuItem="item">
|
||||
<AgentChannelTeamsOptionLabel
|
||||
:label="item.label"
|
||||
:description="item.description"
|
||||
/>
|
||||
</template>
|
||||
<template #activator>
|
||||
<N8nButton
|
||||
variant="solid"
|
||||
|
|
@ -1023,33 +967,22 @@ const downloadPackage = async () =>
|
|||
user does there and then says they are finished with. n8n cannot
|
||||
see the upload, so nothing here claims it happened.
|
||||
-->
|
||||
<div
|
||||
v-else-if="step.id === 'finish' && statusOf[step.id] === 'active'"
|
||||
:class="$style.stepContent"
|
||||
>
|
||||
<!--
|
||||
What is left to do depends on the route taken. A published app is
|
||||
in nobody's Teams until Microsoft lists it, so the package stays
|
||||
on offer as the way to have it sooner.
|
||||
-->
|
||||
<template v-if="installRoute === 'downloaded'">
|
||||
<N8nText size="small" color="text-light" data-testid="teams-finish-instructions">
|
||||
{{ i18n.baseText('agents.channels.teams.managed.install.downloaded') }}
|
||||
</N8nText>
|
||||
<N8nText size="small" color="text-light" data-testid="teams-finish-upload-blocked">
|
||||
{{ i18n.baseText('agents.channels.teams.managed.install.noUploadOption') }}
|
||||
</N8nText>
|
||||
</template>
|
||||
<template v-else-if="published">
|
||||
<N8nText size="small" color="text-light" data-testid="teams-finish-published">
|
||||
{{ i18n.baseText('agents.channels.teams.managed.finish.published') }}
|
||||
<div v-else-if="step.id === 'finish'" :class="$style.stepContent">
|
||||
<template v-if="statusOf[step.id] === 'active'">
|
||||
<N8nText
|
||||
v-if="!awaitingCatalog"
|
||||
size="small"
|
||||
color="text-light"
|
||||
data-testid="teams-finish-upload-hint"
|
||||
>
|
||||
{{ i18n.baseText('agents.channels.teams.setup.install.hint') }}
|
||||
</N8nText>
|
||||
<!--
|
||||
One sentence with the action inside it. A button here carries
|
||||
its own padding, which pushes the line out of alignment with
|
||||
the paragraph above and reads as a second block.
|
||||
-->
|
||||
<N8nText size="small" color="text-light" data-testid="teams-finish-sooner">
|
||||
<N8nText v-else size="small" color="text-light" data-testid="teams-finish-sooner">
|
||||
{{ i18n.baseText('agents.channels.teams.managed.finish.soonerBefore') }}
|
||||
<N8nLink
|
||||
theme="text"
|
||||
|
|
@ -1063,20 +996,71 @@ const downloadPackage = async () =>
|
|||
{{ i18n.baseText('agents.channels.teams.managed.finish.soonerAfter') }}
|
||||
</N8nText>
|
||||
</template>
|
||||
<N8nText v-else size="small" color="text-light" data-testid="teams-finish-instructions">
|
||||
{{ i18n.baseText('agents.channels.teams.managed.install.downloaded') }}
|
||||
</N8nText>
|
||||
<N8nButton
|
||||
variant="outline"
|
||||
size="medium"
|
||||
:disabled="statusOf[step.id] !== 'active'"
|
||||
:loading="loading"
|
||||
data-testid="teams-managed-done"
|
||||
@click="emit('done')"
|
||||
>
|
||||
{{ i18n.baseText('agents.channels.teams.setup.install.done') }}
|
||||
</N8nButton>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="statusOf[step.id] === 'locked'"
|
||||
:class="[$style.stepContent, $style.locked]"
|
||||
inert
|
||||
:class="$style.stepContent"
|
||||
:data-testid="`teams-managed-preview-${step.id}`"
|
||||
>
|
||||
<!-- Greyed out rather than hidden, the way the manual setup marks a
|
||||
step nobody can start yet. -->
|
||||
<N8nText size="small" color="text-light" :data-testid="`teams-managed-locked-${step.id}`">
|
||||
{{ lockedHints[step.id] }}
|
||||
<div :class="$style.preview" inert>
|
||||
<N8nButton v-if="step.id === 'create-app'" variant="outline" size="medium" disabled>
|
||||
{{ i18n.baseText('agents.channels.teams.managed.createApp.button') }}
|
||||
</N8nButton>
|
||||
<N8nButton
|
||||
v-else-if="step.id === 'create-bot'"
|
||||
variant="outline"
|
||||
size="medium"
|
||||
disabled
|
||||
>
|
||||
{{ i18n.baseText('agents.channels.teams.managed.createBot.create') }}
|
||||
</N8nButton>
|
||||
<AgentChannelTeamsAvailability
|
||||
v-else-if="step.id === 'availability'"
|
||||
:model-value="availability"
|
||||
start-collapsed
|
||||
hide-file-access
|
||||
/>
|
||||
<N8nButton
|
||||
v-else-if="step.id === 'file-access'"
|
||||
variant="outline"
|
||||
size="medium"
|
||||
disabled
|
||||
>
|
||||
{{ i18n.baseText('agents.channels.teams.managed.fileAccess.button') }}
|
||||
<N8nIcon icon="chevron-down" size="small" />
|
||||
</N8nButton>
|
||||
<AgentChannelTeamsIdentityCard
|
||||
v-else-if="step.id === 'install'"
|
||||
:name="identityName"
|
||||
:description="identityDescription"
|
||||
ready
|
||||
>
|
||||
<template #action>
|
||||
<N8nButton variant="outline" size="medium" disabled>
|
||||
{{ i18n.baseText('agents.channels.teams.managed.install.menu') }}
|
||||
<N8nIcon icon="chevron-down" size="small" />
|
||||
</N8nButton>
|
||||
</template>
|
||||
</AgentChannelTeamsIdentityCard>
|
||||
</div>
|
||||
<N8nText
|
||||
v-if="reconnectRequired"
|
||||
size="small"
|
||||
color="text-light"
|
||||
:data-testid="`teams-managed-locked-${step.id}`"
|
||||
>
|
||||
{{ i18n.baseText('agents.channels.teams.managed.connect.lockedBySignIn') }}
|
||||
</N8nText>
|
||||
</div>
|
||||
|
||||
|
|
@ -1090,6 +1074,16 @@ const downloadPackage = async () =>
|
|||
:data-testid="`teams-managed-error-${step.id}`"
|
||||
>
|
||||
{{ errorMessage }}
|
||||
<N8nLink
|
||||
v-if="step.id === 'create-app'"
|
||||
theme="danger"
|
||||
size="small"
|
||||
underline
|
||||
data-testid="teams-use-own-app-from-app-step"
|
||||
@click="useOwnMicrosoftApp"
|
||||
>
|
||||
{{ i18n.baseText('agents.channels.teams.managed.useOwnApp') }}
|
||||
</N8nLink>
|
||||
</N8nText>
|
||||
</template>
|
||||
</N8nStepper>
|
||||
|
|
@ -1117,9 +1111,12 @@ const downloadPackage = async () =>
|
|||
gap: var(--spacing--2xs);
|
||||
}
|
||||
|
||||
.locked {
|
||||
.preview {
|
||||
opacity: 0.45;
|
||||
pointer-events: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.stepContent {
|
||||
|
|
@ -1142,13 +1139,6 @@ const downloadPackage = async () =>
|
|||
padding-top: var(--spacing--2xs);
|
||||
}
|
||||
|
||||
.summaryLine {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing--4xs);
|
||||
}
|
||||
|
||||
.field {
|
||||
width: 100%;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
<script setup lang="ts">
|
||||
import { N8nText } from '@n8n/design-system';
|
||||
|
||||
defineProps<{ label: string; description?: string }>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span :class="$style.option">
|
||||
<N8nText size="small" bold color="text-dark">{{ label }}</N8nText>
|
||||
<N8nText v-if="description" size="xsmall" color="text-light">{{ description }}</N8nText>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style module lang="scss">
|
||||
.option {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing--5xs);
|
||||
max-width: 260px;
|
||||
padding: var(--spacing--4xs) 0;
|
||||
white-space: normal;
|
||||
line-height: var(--line-height--md);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -583,13 +583,12 @@ describe('AgentChannelTeamsSetup', () => {
|
|||
expect(emitted().connect).toBeFalsy();
|
||||
});
|
||||
|
||||
it('names the download step and what the package is built from', async () => {
|
||||
it('names the download step', async () => {
|
||||
const { getByText } = renderComponent({ props: props() });
|
||||
|
||||
await waitFor(() =>
|
||||
expect(getByText('agents.channels.teams.setup.download.title')).toBeVisible(),
|
||||
);
|
||||
expect(getByText('agents.channels.teams.setup.download.description')).toBeVisible();
|
||||
});
|
||||
|
||||
it('reports a failed download and does not connect', async () => {
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import type { AgentCredentialOption } from '../../components/AgentCredentialSele
|
|||
import AgentChannelTeamsAvailability from './AgentChannelTeamsAvailability.vue';
|
||||
import AgentChannelTeamsManagedApp from './AgentChannelTeamsManagedApp.vue';
|
||||
import type { TeamsChannelRuntime } from './useTeamsChannelRuntime';
|
||||
import { useTeamsUploadInstruction } from './useTeamsUploadInstruction';
|
||||
import {
|
||||
availabilityFrom,
|
||||
AZURE_BOTS_URL,
|
||||
|
|
@ -360,25 +361,7 @@ const availabilityLocked = computed(
|
|||
|
||||
const canFinish = computed(() => downloadedFor.value === credentialId.value && props.connected);
|
||||
|
||||
function addToText(scopes?: { teamChannels?: boolean; groupChats?: boolean }) {
|
||||
if (scopes?.teamChannels && scopes.groupChats)
|
||||
return i18n.baseText('agents.channels.teams.setup.install.addTo.both');
|
||||
if (scopes?.teamChannels) return i18n.baseText('agents.channels.teams.setup.install.addTo.teams');
|
||||
if (scopes?.groupChats) return i18n.baseText('agents.channels.teams.setup.install.addTo.chats');
|
||||
return null;
|
||||
}
|
||||
|
||||
function uploadInstruction(scopes?: { teamChannels?: boolean; groupChats?: boolean }) {
|
||||
const upload = i18n.baseText('agents.channels.teams.setup.install.description');
|
||||
const addTo = addToText(scopes);
|
||||
// Composed through a key rather than joined here, so a locale owns both the
|
||||
// separator and the order of the two sentences.
|
||||
return addTo
|
||||
? i18n.baseText('agents.channels.teams.setup.install.descriptionWithScope', {
|
||||
interpolate: { upload, addTo },
|
||||
})
|
||||
: upload;
|
||||
}
|
||||
const uploadInstruction = useTeamsUploadInstruction();
|
||||
|
||||
// The modal clears the error when the credential changes. A conflict is about
|
||||
// the credential itself, so it stays in step 2.
|
||||
|
|
@ -527,7 +510,6 @@ const steps = computed(() => [
|
|||
{
|
||||
id: 'download',
|
||||
title: i18n.baseText('agents.channels.teams.setup.download.title'),
|
||||
description: i18n.baseText('agents.channels.teams.setup.download.description'),
|
||||
},
|
||||
{
|
||||
id: 'install',
|
||||
|
|
@ -878,7 +860,7 @@ defineExpose({
|
|||
{{ i18n.baseText('agents.channels.teams.setup.publishNotice') }}
|
||||
</N8nText>
|
||||
<N8nButton
|
||||
variant="solid"
|
||||
variant="outline"
|
||||
size="medium"
|
||||
:disabled="!canFinish"
|
||||
data-testid="teams-done"
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useI18n } from '@n8n/i18n';
|
|||
import { computed } from 'vue';
|
||||
|
||||
import type { AgentChannelRuntime } from '../types';
|
||||
import AgentChannelTeamsOptionLabel from './AgentChannelTeamsOptionLabel.vue';
|
||||
import { isTeamsChannelRuntime, type TeamsSetupKind } from './useTeamsChannelRuntime';
|
||||
|
||||
const props = defineProps<{
|
||||
|
|
@ -26,6 +27,19 @@ const setupKind = computed<TeamsSetupKind>({
|
|||
}
|
||||
},
|
||||
});
|
||||
|
||||
const options = computed(() => [
|
||||
{
|
||||
value: 'managed',
|
||||
label: i18n.baseText('agents.channels.teams.setupKind.recommended'),
|
||||
description: i18n.baseText('agents.channels.teams.setupKind.recommendedDescription'),
|
||||
},
|
||||
{
|
||||
value: 'manual',
|
||||
label: i18n.baseText('agents.channels.teams.setupKind.manual'),
|
||||
description: i18n.baseText('agents.channels.teams.setupKind.manualDescription'),
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -38,11 +52,9 @@ const setupKind = computed<TeamsSetupKind>({
|
|||
:teleported="false"
|
||||
data-testid="teams-setup-kind-selector"
|
||||
>
|
||||
<N8nOption
|
||||
value="managed"
|
||||
:label="i18n.baseText('agents.channels.teams.setupKind.recommended')"
|
||||
/>
|
||||
<N8nOption value="manual" :label="i18n.baseText('agents.channels.teams.setupKind.manual')" />
|
||||
<N8nOption v-for="opt in options" :key="opt.value" :value="opt.value" :label="opt.label">
|
||||
<AgentChannelTeamsOptionLabel :label="opt.label" :description="opt.description" />
|
||||
</N8nOption>
|
||||
</N8nSelect>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -49,10 +49,10 @@ vi.mock('@/features/credentials/credentials.store', async (importOriginal) => {
|
|||
|
||||
const ensureAgentPersisted = vi.fn();
|
||||
|
||||
const buildRuntime = () =>
|
||||
const buildRuntime = (agentId = ref('agent-1')) =>
|
||||
useTeamsChannelRuntime({
|
||||
projectId: ref('project-1'),
|
||||
agentId: ref('agent-1'),
|
||||
agentId,
|
||||
selectedCredentialId: ref(''),
|
||||
credentialModalOpen: computed(() => false),
|
||||
fetchStatus: vi.fn(),
|
||||
|
|
@ -70,6 +70,7 @@ describe('useTeamsChannelRuntime', () => {
|
|||
managerCredentials: [],
|
||||
adminConsentUrl: null,
|
||||
});
|
||||
vi.mocked(getTeamsSetupState).mockResolvedValue({ botId: null } as never);
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
@ -92,6 +93,60 @@ describe('useTeamsChannelRuntime', () => {
|
|||
expect(runtime.loading.value).toBe(false);
|
||||
});
|
||||
|
||||
it('reads the agent identity on load, before any app exists', async () => {
|
||||
vi.mocked(getTeamsSetupState).mockResolvedValue({
|
||||
defaultDisplayName: 'Support Agent',
|
||||
} as never);
|
||||
const runtime = buildRuntime();
|
||||
|
||||
await runtime.load();
|
||||
|
||||
await vi.waitFor(() =>
|
||||
expect(runtime.botSetupState.value).toMatchObject({ defaultDisplayName: 'Support Agent' }),
|
||||
);
|
||||
expect(getTeamsSetupState).toHaveBeenCalledWith(expect.anything(), 'project-1', 'agent-1');
|
||||
});
|
||||
|
||||
it('reads the identity again for another agent', async () => {
|
||||
const agentId = ref('agent-1');
|
||||
vi.mocked(getTeamsSetupState).mockResolvedValueOnce({
|
||||
defaultDisplayName: 'First Agent',
|
||||
} as never);
|
||||
const runtime = buildRuntime(agentId);
|
||||
await runtime.load();
|
||||
await vi.waitFor(() =>
|
||||
expect(runtime.botSetupState.value).toMatchObject({ defaultDisplayName: 'First Agent' }),
|
||||
);
|
||||
|
||||
agentId.value = 'agent-2';
|
||||
vi.mocked(getTeamsSetupState).mockResolvedValueOnce({
|
||||
defaultDisplayName: 'Second Agent',
|
||||
} as never);
|
||||
await runtime.load();
|
||||
|
||||
await vi.waitFor(() =>
|
||||
expect(runtime.botSetupState.value).toMatchObject({ defaultDisplayName: 'Second Agent' }),
|
||||
);
|
||||
expect(getTeamsSetupState).toHaveBeenLastCalledWith(expect.anything(), 'project-1', 'agent-2');
|
||||
});
|
||||
|
||||
it('reads the identity again once an agent that was not saved yet is', async () => {
|
||||
vi.mocked(getTeamsSetupState)
|
||||
.mockRejectedValueOnce(new Error('not found'))
|
||||
.mockResolvedValueOnce({ defaultDisplayName: 'Support Agent' } as never);
|
||||
const runtime = buildRuntime();
|
||||
|
||||
await runtime.load();
|
||||
await vi.waitFor(() => expect(getTeamsSetupState).toHaveBeenCalledTimes(1));
|
||||
expect(runtime.botSetupState.value).toBeNull();
|
||||
|
||||
await runtime.load();
|
||||
|
||||
await vi.waitFor(() =>
|
||||
expect(runtime.botSetupState.value).toMatchObject({ defaultDisplayName: 'Support Agent' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('stops reporting loading when the setup state cannot be read', async () => {
|
||||
vi.mocked(getTeamsManagedSetup).mockRejectedValue(new Error('offline'));
|
||||
const runtime = buildRuntime();
|
||||
|
|
|
|||
|
|
@ -91,6 +91,8 @@ export function useTeamsChannelRuntime(context: AgentChannelRuntimeContext): Tea
|
|||
const loading = ref(false);
|
||||
const provisionedApp = ref<TeamsProvisionedAppSummary | null>(null);
|
||||
const botSetupState = ref<TeamsAgentSetupState | null>(null);
|
||||
// The modal keeps this runtime across agents, so the state says whose it is.
|
||||
const botSetupStateFor = ref('');
|
||||
const provisionedBot = ref<TeamsProvisionedBotSummary | null>(null);
|
||||
const subscriptions = ref<TeamsAzureSubscription[]>([]);
|
||||
const catalogState = ref<TeamsCatalogState | null>(null);
|
||||
|
|
@ -140,6 +142,24 @@ export function useTeamsChannelRuntime(context: AgentChannelRuntimeContext): Tea
|
|||
const isCurrent = () =>
|
||||
requestedFor === [context.projectId.value, context.agentId.value].join('|');
|
||||
loading.value = true;
|
||||
// Without a credential, so the install card has the agent's name before any
|
||||
// app exists. Skipped once this agent has one: provisioning's read is for
|
||||
// its credential. An agent not saved yet has nothing to read until the
|
||||
// sign-in saves it and loads again.
|
||||
if (botSetupStateFor.value !== requestedFor) {
|
||||
botSetupState.value = null;
|
||||
void getTeamsSetupState(
|
||||
rootStore.restApiContext,
|
||||
context.projectId.value,
|
||||
context.agentId.value,
|
||||
)
|
||||
.then((state) => {
|
||||
if (!isCurrent() || botSetupStateFor.value === requestedFor) return;
|
||||
botSetupState.value = state;
|
||||
botSetupStateFor.value = requestedFor;
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
try {
|
||||
const state = await getTeamsManagedSetup(
|
||||
rootStore.restApiContext,
|
||||
|
|
@ -263,6 +283,7 @@ export function useTeamsChannelRuntime(context: AgentChannelRuntimeContext): Tea
|
|||
);
|
||||
if (readFor !== contextKey()) return false;
|
||||
botSetupState.value = setupState;
|
||||
botSetupStateFor.value = [context.projectId.value, context.agentId.value].join('|');
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
import { useI18n } from '@n8n/i18n';
|
||||
|
||||
type Scopes = { teamChannels?: boolean; groupChats?: boolean };
|
||||
|
||||
export function useTeamsUploadInstruction() {
|
||||
const i18n = useI18n();
|
||||
|
||||
function addToText(scopes?: Scopes) {
|
||||
if (scopes?.teamChannels && scopes.groupChats)
|
||||
return i18n.baseText('agents.channels.teams.setup.install.addTo.both');
|
||||
if (scopes?.teamChannels)
|
||||
return i18n.baseText('agents.channels.teams.setup.install.addTo.teams');
|
||||
if (scopes?.groupChats) return i18n.baseText('agents.channels.teams.setup.install.addTo.chats');
|
||||
return null;
|
||||
}
|
||||
|
||||
return function uploadInstruction(scopes?: Scopes) {
|
||||
const upload = i18n.baseText('agents.channels.teams.setup.install.description');
|
||||
const addTo = addToText(scopes);
|
||||
// Composed through a key rather than joined here, so a locale owns both the
|
||||
// separator and the order of the two sentences.
|
||||
return addTo
|
||||
? i18n.baseText('agents.channels.teams.setup.install.descriptionWithScope', {
|
||||
interpolate: { upload, addTo },
|
||||
})
|
||||
: upload;
|
||||
};
|
||||
}
|
||||
|
|
@ -321,17 +321,13 @@ watch(
|
|||
|
||||
// n8n Chat's setup view also needs a footer ("Make available"), unlike every
|
||||
// other channel's setup, which connects straight from its own view.
|
||||
/**
|
||||
* A setup that outlives its connect gets the footer too, so its Done button
|
||||
* sits where every other dialog's does rather than at the end of the scroll.
|
||||
*/
|
||||
const setupCanFinish = computed(
|
||||
() => isSetupMode.value && channelViewRef.value?.canFinish === true,
|
||||
);
|
||||
const showFooterActions = computed(
|
||||
() =>
|
||||
selectedChannelType.value !== null &&
|
||||
(isEditMode.value || (isSetupMode.value && isN8nChatSelected.value) || setupCanFinish.value),
|
||||
(isEditMode.value || (isSetupMode.value && isN8nChatSelected.value)),
|
||||
);
|
||||
|
||||
const currentChannelCredentialId = computed(() =>
|
||||
|
|
@ -419,6 +415,7 @@ function completeAndClose() {
|
|||
* from the agent. Staying open on a failure keeps the reason on screen.
|
||||
*/
|
||||
async function finishSetup() {
|
||||
if (!setupCanFinish.value) return completeAndClose();
|
||||
if (await writeChannelConfig()) completeAndClose();
|
||||
}
|
||||
|
||||
|
|
@ -724,7 +721,7 @@ watch(
|
|||
@connect="saveChannelConfig"
|
||||
@persist="writeChannelConfig"
|
||||
@connected="handlePlatformConnected"
|
||||
@done="completeAndClose"
|
||||
@done="finishSetup"
|
||||
/>
|
||||
<N8nText
|
||||
v-if="saveAttempted && !currentChannelCredentialId"
|
||||
|
|
@ -758,17 +755,7 @@ watch(
|
|||
</N8nButton>
|
||||
</template>
|
||||
<template v-if="showFooterActions" #footerActions>
|
||||
<N8nButton
|
||||
v-if="setupCanFinish"
|
||||
variant="solid"
|
||||
size="medium"
|
||||
:loading="actionInFlight"
|
||||
data-testid="agent-channel-setup-done"
|
||||
@click="finishSetup"
|
||||
>
|
||||
{{ i18n.baseText('generic.done') }}
|
||||
</N8nButton>
|
||||
<template v-else-if="isN8nChatSelected">
|
||||
<template v-if="isN8nChatSelected">
|
||||
<N8nButton
|
||||
variant="ghost"
|
||||
size="medium"
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user