1
1
mirror of https://github.com/n8n-io/n8n.git synced 2024-10-06 01:27:49 +03:00

feat(editor): Overhaul document title management (#10999)

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™ 2024-09-30 10:57:25 +02:00 committed by GitHub
parent 63e6f1fa38
commit bb2895689f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 155 additions and 83 deletions

View File

@ -34,7 +34,7 @@ import { useWorkflowsStore } from '@/stores/workflows.store';
import { useProjectsStore } from '@/stores/projects.store';
import { saveAs } from 'file-saver';
import { useTitleChange } from '@/composables/useTitleChange';
import { useDocumentTitle } from '@/composables/useDocumentTitle';
import { useMessage } from '@/composables/useMessage';
import { useToast } from '@/composables/useToast';
import { getResourcePermissions } from '@/permissions';
@ -87,7 +87,7 @@ const locale = useI18n();
const telemetry = useTelemetry();
const message = useMessage();
const toast = useToast();
const titleChange = useTitleChange();
const documentTitle = useDocumentTitle();
const workflowHelpers = useWorkflowHelpers({ router });
const isTagsEditEnabled = ref(false);
@ -558,7 +558,7 @@ async function onWorkflowMenuSelect(action: WORKFLOW_MENU_ACTIONS): Promise<void
}
uiStore.stateIsDirty = false;
// Reset tab title since workflow is deleted.
titleChange.titleReset();
documentTitle.reset();
toast.showMessage({
title: locale.baseText('mainSidebar.showMessage.handleSelect1.title'),
type: 'success',

View File

@ -10,7 +10,7 @@ import { useI18n } from '@/composables/useI18n';
import { useToast } from '@/composables/useToast';
import { useUIStore } from '@/stores/ui.store';
import { useOrchestrationStore } from '@/stores/orchestration.store';
import { setPageTitle } from '@/utils/htmlUtils';
import { useDocumentTitle } from '@/composables/useDocumentTitle';
import WorkerCard from './Workers/WorkerCard.ee.vue';
import { usePushConnection } from '@/composables/usePushConnection';
import { usePushConnectionStore } from '@/stores/pushConnection.store';
@ -36,6 +36,7 @@ export default defineComponent({
i18n,
pushConnection,
...useToast(),
documentTitle: useDocumentTitle(),
};
},
computed: {
@ -58,7 +59,7 @@ export default defineComponent({
},
},
mounted() {
setPageTitle(`n8n - ${this.pageTitle}`);
this.documentTitle.set(this.pageTitle);
this.$telemetry.track('User viewed worker view', {
instance_id: this.rootStore.instanceId,

View File

@ -0,0 +1,31 @@
import { mock } from 'vitest-mock-extended';
import { describe, it, expect } from 'vitest';
import type { FrontendSettings } from '@n8n/api-types';
import { useDocumentTitle } from '../useDocumentTitle';
const settings = mock<FrontendSettings>({ releaseChannel: 'stable' });
vi.mock('@/stores/settings.store', () => ({
useSettingsStore: vi.fn(() => ({ settings })),
}));
describe('useDocumentTitle', () => {
it('should set the document title', () => {
const { set } = useDocumentTitle();
set('Test Title');
expect(document.title).toBe('Test Title - n8n');
});
it('should reset the document title', () => {
const { set, reset } = useDocumentTitle();
set('Test Title');
reset();
expect(document.title).toBe('Workflow Automation - n8n');
});
it('should use the correct prefix for the release channel', () => {
settings.releaseChannel = 'beta';
const { set } = useDocumentTitle();
set('Test Title');
expect(document.title).toBe('Test Title - n8n[BETA]');
});
});

View File

@ -52,6 +52,7 @@ vi.mock('@/composables/useWorkflowHelpers', () => ({
getCurrentWorkflow: vi.fn(),
saveCurrentWorkflow: vi.fn(),
getWorkflowDataToSave: vi.fn(),
setDocumentTitle: vi.fn(),
}),
}));
@ -61,10 +62,6 @@ vi.mock('@/composables/useNodeHelpers', () => ({
}),
}));
vi.mock('@/composables/useTitleChange', () => ({
useTitleChange: vi.fn().mockReturnValue({ titleSet: vi.fn() }),
}));
vi.mock('vue-router', async (importOriginal) => {
const { RouterLink } = await importOriginal<typeof router>();
return {

View File

@ -0,0 +1,21 @@
import { useSettingsStore } from '@/stores/settings.store';
const DEFAULT_TITLE = 'Workflow Automation';
export function useDocumentTitle() {
const settingsStore = useSettingsStore();
const { releaseChannel } = settingsStore.settings;
const suffix =
!releaseChannel || releaseChannel === 'stable' ? 'n8n' : `n8n[${releaseChannel.toUpperCase()}]`;
const set = (title: string) => {
const sections = [title || DEFAULT_TITLE, suffix];
document.title = sections.join(' - ');
};
const reset = () => {
set('');
};
return { set, reset };
}

View File

@ -18,7 +18,6 @@ import type { PushMessage, PushPayload } from '@n8n/api-types';
import type { IExecutionResponse, IExecutionsCurrentSummaryExtended } from '@/Interface';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { useTitleChange } from '@/composables/useTitleChange';
import { useToast } from '@/composables/useToast';
import { WORKFLOW_SETTINGS_MODAL_KEY } from '@/constants';
import { getTriggerNodeServiceName } from '@/utils/nodeTypesUtils';
@ -43,7 +42,6 @@ type IPushDataExecutionFinishedPayload = PushPayload<'executionFinished'>;
export function usePushConnection({ router }: { router: ReturnType<typeof useRouter> }) {
const workflowHelpers = useWorkflowHelpers({ router });
const nodeHelpers = useNodeHelpers();
const titleChange = useTitleChange();
const toast = useToast();
const i18n = useI18n();
const telemetry = useTelemetry();
@ -324,7 +322,7 @@ export function usePushConnection({ router }: { router: ReturnType<typeof useRou
}
// Workflow did start but had been put to wait
titleChange.titleSet(workflow.name as string, 'IDLE');
workflowHelpers.setDocumentTitle(workflow.name as string, 'IDLE');
toast.showToast({
title: 'Workflow started waiting',
message: `${action} <a href="https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.wait/" target="_blank">More info</a>`,
@ -333,7 +331,7 @@ export function usePushConnection({ router }: { router: ReturnType<typeof useRou
dangerouslyUseHTMLString: true,
});
} else if (runDataExecuted.finished !== true) {
titleChange.titleSet(workflow.name as string, 'ERROR');
workflowHelpers.setDocumentTitle(workflow.name as string, 'ERROR');
if (
runDataExecuted.data.resultData.error?.name === 'ExpressionError' &&
@ -442,7 +440,7 @@ export function usePushConnection({ router }: { router: ReturnType<typeof useRou
}
} else {
// Workflow did execute without a problem
titleChange.titleSet(workflow.name as string, 'IDLE');
workflowHelpers.setDocumentTitle(workflow.name as string, 'IDLE');
const execution = workflowsStore.getWorkflowExecution;
if (execution?.executedNode) {

View File

@ -20,7 +20,6 @@ import { useToast } from '@/composables/useToast';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { CHAT_TRIGGER_NODE_TYPE, WORKFLOW_LM_CHAT_MODAL_KEY } from '@/constants';
import { useTitleChange } from '@/composables/useTitleChange';
import { useRootStore } from '@/stores/root.store';
import { useUIStore } from '@/stores/ui.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
@ -41,7 +40,6 @@ export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType<typeof u
const workflowHelpers = useWorkflowHelpers({ router: useRunWorkflowOpts.router });
const i18n = useI18n();
const toast = useToast();
const { titleSet } = useTitleChange();
const rootStore = useRootStore();
const uiStore = useUIStore();
@ -93,7 +91,7 @@ export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType<typeof u
return;
}
titleSet(workflow.name as string, 'EXECUTING');
workflowHelpers.setDocumentTitle(workflow.name as string, 'EXECUTING');
toast.clearAllStickyNotifications();
@ -309,7 +307,7 @@ export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType<typeof u
return runWorkflowApiResponse;
} catch (error) {
titleSet(workflow.name as string, 'ERROR');
workflowHelpers.setDocumentTitle(workflow.name as string, 'ERROR');
toast.showError(error, i18n.baseText('workflowRun.showError.title'));
return undefined;
}
@ -387,7 +385,7 @@ export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType<typeof u
workflowsStore.executingNode.length = 0;
uiStore.removeActiveAction('workflowRunning');
titleSet(workflowsStore.workflowName, 'IDLE');
workflowHelpers.setDocumentTitle(workflowsStore.workflowName, 'IDLE');
toast.showMessage({
title: i18n.baseText('nodeView.showMessage.stopExecutionCatch.unsaved.title'),
message: i18n.baseText('nodeView.showMessage.stopExecutionCatch.unsaved.message'),
@ -408,7 +406,7 @@ export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType<typeof u
retryOf: execution.retryOf,
};
workflowsStore.finishActiveExecution(pushData);
titleSet(execution.workflowData.name, 'IDLE');
workflowHelpers.setDocumentTitle(execution.workflowData.name, 'IDLE');
workflowsStore.executingNode.length = 0;
workflowsStore.setWorkflowExecutionData(executedData as IExecutionResponse);
uiStore.removeActiveAction('workflowRunning');

View File

@ -1,30 +0,0 @@
import type { WorkflowTitleStatus } from '@/Interface';
import { useSettingsStore } from '@/stores/settings.store';
export function useTitleChange() {
const prependBeta = (title: string) => {
const settingsStore = useSettingsStore();
const { releaseChannel } = settingsStore.settings;
return releaseChannel === 'stable' ? title : `[${releaseChannel.toUpperCase()}] ${title}`;
};
const titleSet = (workflow: string, status: WorkflowTitleStatus) => {
let icon = '⚠️';
if (status === 'EXECUTING') {
icon = '🔄';
} else if (status === 'IDLE') {
icon = '▶️';
}
window.document.title = prependBeta(`n8n - ${icon} ${workflow}`);
};
const titleReset = () => {
window.document.title = prependBeta('n8n - Workflow Automation');
};
return {
titleSet,
titleReset,
};
}

View File

@ -37,6 +37,7 @@ import type {
IWorkflowDataUpdate,
IWorkflowDb,
TargetItem,
WorkflowTitleStatus,
XYPosition,
} from '@/Interface';
@ -57,6 +58,7 @@ import { getSourceItems } from '@/utils/pairedItemUtils';
import { v4 as uuid } from 'uuid';
import { useSettingsStore } from '@/stores/settings.store';
import { getCredentialTypeName, isCredentialOnlyNodeType } from '@/utils/credentialOnlyNodes';
import { useDocumentTitle } from '@/composables/useDocumentTitle';
import { useExternalHooks } from '@/composables/useExternalHooks';
import { useCanvasStore } from '@/stores/canvas.store';
import { useSourceControlStore } from '@/stores/sourceControl.store';
@ -458,6 +460,17 @@ export function useWorkflowHelpers(options: { router: ReturnType<typeof useRoute
const message = useMessage();
const i18n = useI18n();
const telemetry = useTelemetry();
const documentTitle = useDocumentTitle();
const setDocumentTitle = (workflowName: string, status: WorkflowTitleStatus) => {
let icon = '⚠️';
if (status === 'EXECUTING') {
icon = '🔄';
} else if (status === 'IDLE') {
icon = '▶️';
}
documentTitle.set(`${icon} ${workflowName}`);
};
function getNodeTypesMaxCount() {
const nodes = workflowsStore.allNodes;
@ -1172,6 +1185,7 @@ export function useWorkflowHelpers(options: { router: ReturnType<typeof useRoute
}
return {
setDocumentTitle,
resolveParameter,
resolveRequiredParameters,
getCurrentWorkflow,

View File

@ -17,7 +17,6 @@ import { useUIStore } from './ui.store';
import { useUsersStore } from './users.store';
import { useVersionsStore } from './versions.store';
import { makeRestApiRequest } from '@/utils/apiUtils';
import { useTitleChange } from '@/composables/useTitleChange';
import { useToast } from '@/composables/useToast';
import { i18n } from '@/plugins/i18n';
@ -258,9 +257,6 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
ExpressionEvaluatorProxy.setEvaluator(settings.value.expressions.evaluator);
// Re-compute title since settings are now available
useTitleChange().titleReset();
initialized.value = true;
} catch (e) {
showToast({

View File

@ -48,10 +48,6 @@ export const sanitizeIfString = <T>(message: T): string | T => {
return message;
};
export function setPageTitle(title: string) {
window.document.title = title;
}
export function convertRemToPixels(rem: string) {
return parseInt(rem, 10) * parseFloat(getComputedStyle(document.documentElement).fontSize);
}

View File

@ -18,6 +18,7 @@ import ProjectTabs from '@/components/Projects/ProjectTabs.vue';
import useEnvironmentsStore from '@/stores/environments.ee.store';
import { useSettingsStore } from '@/stores/settings.store';
import { getResourcePermissions } from '@/permissions';
import { useDocumentTitle } from '@/composables/useDocumentTitle';
export default defineComponent({
name: 'CredentialsView',
@ -35,6 +36,7 @@ export default defineComponent({
},
sourceControlStoreUnsubscribe: () => {},
loading: false,
documentTitle: useDocumentTitle(),
};
},
computed: {
@ -86,6 +88,7 @@ export default defineComponent({
},
},
mounted() {
this.documentTitle.set(this.$locale.baseText('credentials.heading'));
this.sourceControlStoreUnsubscribe = this.sourceControlStore.$onAction(({ name, after }) => {
if (name === 'pullWorkfolder' && after) {
after(() => {

View File

@ -1,13 +1,13 @@
<script lang="ts" setup>
import { onBeforeMount, onBeforeUnmount, onMounted } from 'vue';
import GlobalExecutionsList from '@/components/executions/global/GlobalExecutionsList.vue';
import { setPageTitle } from '@/utils/htmlUtils';
import { useI18n } from '@/composables/useI18n';
import { useTelemetry } from '@/composables/useTelemetry';
import { useExternalHooks } from '@/composables/useExternalHooks';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useExecutionsStore } from '@/stores/executions.store';
import { useToast } from '@/composables/useToast';
import { useDocumentTitle } from '@/composables/useDocumentTitle';
import { storeToRefs } from 'pinia';
import type { ExecutionFilterType } from '@/Interface';
@ -16,6 +16,7 @@ const telemetry = useTelemetry();
const externalHooks = useExternalHooks();
const workflowsStore = useWorkflowsStore();
const executionsStore = useExecutionsStore();
const documentTitle = useDocumentTitle();
const toast = useToast();
@ -32,7 +33,7 @@ onBeforeMount(async () => {
});
onMounted(async () => {
setPageTitle(`n8n - ${i18n.baseText('executionsList.workflowExecutions')}`);
documentTitle.set(i18n.baseText('executionsList.workflowExecutions'));
document.addEventListener('visibilitychange', onDocumentVisibilityChange);
await executionsStore.initialize();

View File

@ -78,7 +78,7 @@ import { useCanvasOperations } from '@/composables/useCanvasOperations';
import { useExecutionsStore } from '@/stores/executions.store';
import { useCanvasStore } from '@/stores/canvas.store';
import { useMessage } from '@/composables/useMessage';
import { useTitleChange } from '@/composables/useTitleChange';
import { useDocumentTitle } from '@/composables/useDocumentTitle';
import { useNpsSurveyStore } from '@/stores/npsSurvey.store';
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
import { useTelemetry } from '@/composables/useTelemetry';
@ -122,7 +122,7 @@ const telemetry = useTelemetry();
const externalHooks = useExternalHooks();
const toast = useToast();
const message = useMessage();
const { titleReset, titleSet } = useTitleChange();
const documentTitle = useDocumentTitle();
const workflowHelpers = useWorkflowHelpers({ router });
const nodeHelpers = useNodeHelpers();
@ -371,7 +371,7 @@ async function initializeWorkspaceForExistingWorkflow(id: string) {
async function openWorkflow(data: IWorkflowDb) {
resetWorkspace();
titleSet(editableWorkflow.value.name, 'IDLE');
workflowHelpers.setDocumentTitle(editableWorkflow.value.name, 'IDLE');
await initializeWorkspace(data);
@ -1218,7 +1218,7 @@ async function onSourceControlPull() {
if (workflowId.value && !uiStore.stateIsDirty) {
const workflowData = await workflowsStore.fetchWorkflow(workflowId.value);
if (workflowData) {
titleSet(workflowData.name, 'IDLE');
workflowHelpers.setDocumentTitle(workflowData.name, 'IDLE');
await openWorkflow(workflowData);
}
}
@ -1372,7 +1372,7 @@ function checkIfRouteIsAllowed() {
async function initializeDebugMode() {
if (route.name === VIEWS.EXECUTION_DEBUG) {
titleSet(workflowsStore.workflowName, 'DEBUG');
workflowHelpers.setDocumentTitle(workflowsStore.workflowName, 'DEBUG');
if (!workflowsStore.isInDebugMode) {
await applyExecutionData(route.params.executionId as string);
@ -1487,7 +1487,7 @@ onBeforeMount(() => {
onMounted(() => {
canvasStore.startLoading();
titleReset();
documentTitle.reset();
resetWorkspace();
void initializeData().then(() => {

View File

@ -46,7 +46,7 @@ import { useGlobalLinkActions } from '@/composables/useGlobalLinkActions';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
import useCanvasMouseSelect from '@/composables/useCanvasMouseSelect';
import { useExecutionDebugging } from '@/composables/useExecutionDebugging';
import { useTitleChange } from '@/composables/useTitleChange';
import { useDocumentTitle } from '@/composables/useDocumentTitle';
import { useDataSchema } from '@/composables/useDataSchema';
import { type ContextMenuAction, useContextMenu } from '@/composables/useContextMenu';
import { useUniqueNodeName } from '@/composables/useUniqueNodeName';
@ -258,7 +258,7 @@ export default defineComponent({
callDebounced,
...useCanvasMouseSelect(),
...useGlobalLinkActions(),
...useTitleChange(),
documentTitle: useDocumentTitle(),
...useToast(),
...useMessage(),
...useUniqueNodeName(),
@ -591,7 +591,7 @@ export default defineComponent({
return;
}
this.canvasStore.initInstance(this.nodeViewRef);
this.titleReset();
this.documentTitle.reset();
window.addEventListener('message', this.onPostMessageReceived);
@ -1719,7 +1719,7 @@ export default defineComponent({
this.workflowsStore.executingNode.length = 0;
this.uiStore.removeActiveAction('workflowRunning');
this.titleSet(this.workflowsStore.workflowName, 'IDLE');
this.workflowHelpers.setDocumentTitle(this.workflowsStore.workflowName, 'IDLE');
this.showMessage({
title: this.$locale.baseText('nodeView.showMessage.stopExecutionCatch.unsaved.title'),
message: this.$locale.baseText(
@ -1743,7 +1743,7 @@ export default defineComponent({
retryOf: execution.retryOf,
};
this.workflowsStore.finishActiveExecution(pushData);
this.titleSet(execution.workflowData.name, 'IDLE');
this.workflowHelpers.setDocumentTitle(execution.workflowData.name, 'IDLE');
this.workflowsStore.executingNode.length = 0;
this.workflowsStore.setWorkflowExecutionData(executedData as IExecutionResponse);
this.uiStore.removeActiveAction('workflowRunning');
@ -3410,7 +3410,7 @@ export default defineComponent({
}
if (workflow) {
this.titleSet(workflow.name, 'IDLE');
this.workflowHelpers.setDocumentTitle(workflow.name, 'IDLE');
await this.openWorkflow(workflow);
await this.checkAndInitDebugMode();
@ -4371,7 +4371,7 @@ export default defineComponent({
},
async checkAndInitDebugMode() {
if (this.$route.name === VIEWS.EXECUTION_DEBUG) {
this.titleSet(this.workflowName, 'DEBUG');
this.workflowHelpers.setDocumentTitle(this.workflowName, 'DEBUG');
if (!this.workflowsStore.isInDebugMode) {
await this.applyExecutionData(this.$route.params.executionId as string);
this.workflowsStore.isInDebugMode = true;
@ -4444,7 +4444,7 @@ export default defineComponent({
const workflow: IWorkflowDb | undefined =
await this.workflowsStore.fetchWorkflow(workflowId);
if (workflow) {
this.titleSet(workflow.name, 'IDLE');
this.workflowHelpers.setDocumentTitle(workflow.name, 'IDLE');
await this.openWorkflow(workflow);
}
}

View File

@ -17,6 +17,7 @@ import { useRolesStore } from '@/stores/roles.store';
import type { ProjectRole } from '@/types/roles.types';
import { useCloudPlanStore } from '@/stores/cloudPlan.store';
import { useTelemetry } from '@/composables/useTelemetry';
import { useDocumentTitle } from '@/composables/useDocumentTitle';
type FormDataDiff = {
name?: string;
@ -33,6 +34,7 @@ const cloudPlanStore = useCloudPlanStore();
const toast = useToast();
const router = useRouter();
const telemetry = useTelemetry();
const documentTitle = useDocumentTitle();
const dialogVisible = ref(false);
const upgradeDialogVisible = ref(false);
@ -250,6 +252,7 @@ onBeforeMount(async () => {
});
onMounted(() => {
documentTitle.set(locale.baseText('projects.settings'));
selectProjectNameIfMatchesDefault();
});
</script>

View File

@ -3,6 +3,7 @@ import { defineComponent } from 'vue';
import type { ApiKey, IUser } from '@/Interface';
import { useToast } from '@/composables/useToast';
import { useMessage } from '@/composables/useMessage';
import { useDocumentTitle } from '@/composables/useDocumentTitle';
import CopyInput from '@/components/CopyInput.vue';
import { mapStores } from 'pinia';
@ -23,6 +24,7 @@ export default defineComponent({
...useToast(),
...useMessage(),
...useUIStore(),
documentTitle: useDocumentTitle(),
};
},
data() {
@ -35,6 +37,7 @@ export default defineComponent({
};
},
mounted() {
this.documentTitle.set(this.$locale.baseText('settings.api'));
if (!this.isPublicApiEnabled) return;
void this.getApiKeys();

View File

@ -5,6 +5,7 @@ import {
} from '@/constants';
import CommunityPackageCard from '@/components/CommunityPackageCard.vue';
import { useToast } from '@/composables/useToast';
import { useDocumentTitle } from '@/composables/useDocumentTitle';
import type { PublicInstalledPackage } from 'n8n-workflow';
import { useCommunityNodesStore } from '@/stores/communityNodes.store';
@ -31,6 +32,7 @@ const externalHooks = useExternalHooks();
const i18n = useI18n();
const telemetry = useTelemetry();
const toast = useToast();
const documentTitle = useDocumentTitle();
const communityNodesStore = useCommunityNodesStore();
const uiStore = useUIStore();
@ -85,6 +87,7 @@ onBeforeMount(() => {
});
onMounted(async () => {
documentTitle.set(i18n.baseText('settings.communityNodes'));
try {
loading.value = true;
await communityNodesStore.fetchInstalledPackages();

View File

@ -2,6 +2,7 @@
import { useUIStore } from '@/stores/ui.store';
import { useI18n } from '@/composables/useI18n';
import { useToast } from '@/composables/useToast';
import { useDocumentTitle } from '@/composables/useDocumentTitle';
import { useExternalSecretsStore } from '@/stores/externalSecrets.ee.store';
import { computed, onMounted } from 'vue';
import ExternalSecretsProviderCard from '@/components/ExternalSecretsProviderCard.ee.vue';
@ -11,6 +12,7 @@ const i18n = useI18n();
const uiStore = useUIStore();
const externalSecretsStore = useExternalSecretsStore();
const toast = useToast();
const documentTitle = useDocumentTitle();
const sortedProviders = computed(() => {
return ([...externalSecretsStore.providers] as ExternalSecretsProvider[]).sort((a, b) => {
@ -19,6 +21,7 @@ const sortedProviders = computed(() => {
});
onMounted(() => {
documentTitle.set(i18n.baseText('settings.externalSecrets.title'));
if (!externalSecretsStore.isEnterpriseExternalSecretsEnabled) return;
try {
void externalSecretsStore.fetchAllSecrets();

View File

@ -5,6 +5,7 @@ import { capitalizeFirstLetter } from '@/utils/htmlUtils';
import { convertToDisplayDate } from '@/utils/typesUtils';
import { useToast } from '@/composables/useToast';
import { useMessage } from '@/composables/useMessage';
import { useDocumentTitle } from '@/composables/useDocumentTitle';
import type {
ILdapConfig,
ILdapSyncData,
@ -65,6 +66,7 @@ type CellClassStyleMethodParams<T> = {
const toast = useToast();
const i18n = useI18n();
const message = useMessage();
const documentTitle = useDocumentTitle();
const settingsStore = useSettingsStore();
const uiStore = useUIStore();
@ -585,6 +587,7 @@ const reloadLdapSynchronizations = async () => {
};
onMounted(async () => {
documentTitle.set(i18n.baseText('settings.ldap'));
if (!isLDAPFeatureEnabled.value) return;
await getLdapConfig();
});

View File

@ -13,6 +13,7 @@ import type { MessageEventBusDestinationOptions } from 'n8n-workflow';
import { deepCopy, defaultMessageEventBusDestinationOptions } from 'n8n-workflow';
import EventDestinationCard from '@/components/SettingsLogStreaming/EventDestinationCard.ee.vue';
import { createEventBus } from 'n8n-design-system/utils';
import { useDocumentTitle } from '@/composables/useDocumentTitle';
export default defineComponent({
name: 'SettingsLogStreamingView',
@ -26,9 +27,11 @@ export default defineComponent({
destinations: Array<MessageEventBusDestinationOptions>,
disableLicense: false,
allDestinations: [] as MessageEventBusDestinationOptions[],
documentTitle: useDocumentTitle(),
};
},
async mounted() {
this.documentTitle.set(this.$locale.baseText('settings.log-streaming.heading'));
if (!this.isLicensed) return;
// Prepare credentialsStore so modals can pick up credentials

View File

@ -2,6 +2,7 @@
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
import { useI18n } from '@/composables/useI18n';
import { useToast } from '@/composables/useToast';
import { useDocumentTitle } from '@/composables/useDocumentTitle';
import type { IFormInputs, IUser, ThemeOption } from '@/Interface';
import {
CHANGE_PASSWORD_MODAL_KEY,
@ -28,6 +29,7 @@ type UserBasicDetailsWithMfa = UserBasicDetailsForm & {
const i18n = useI18n();
const { showToast, showError } = useToast();
const documentTitle = useDocumentTitle();
const hasAnyBasicInfoChanges = ref<boolean>(false);
const formInputs = ref<null | IFormInputs>(null);
@ -80,6 +82,7 @@ const hasAnyChanges = computed(() => {
});
onMounted(() => {
documentTitle.set(i18n.baseText('settings.personal.personalSettings'));
formInputs.value = [
{
name: 'firstName',

View File

@ -8,6 +8,7 @@ import { useToast } from '@/composables/useToast';
import { useLoadingService } from '@/composables/useLoadingService';
import { useI18n } from '@/composables/useI18n';
import { useMessage } from '@/composables/useMessage';
import { useDocumentTitle } from '@/composables/useDocumentTitle';
import CopyInput from '@/components/CopyInput.vue';
import type { TupleToUnion } from '@/utils/typeHelpers';
import type { SshKeyTypes } from '@/Interface';
@ -17,6 +18,7 @@ const sourceControlStore = useSourceControlStore();
const uiStore = useUIStore();
const toast = useToast();
const message = useMessage();
const documentTitle = useDocumentTitle();
const loadingService = useLoadingService();
const isConnected = ref(false);
@ -112,6 +114,7 @@ const initialize = async () => {
};
onMounted(async () => {
documentTitle.set(locale.baseText('settings.sourceControl.title'));
if (!sourceControlStore.isEnterpriseSourceControlEnabled) return;
await initialize();
});

View File

@ -7,6 +7,7 @@ import { useI18n } from '@/composables/useI18n';
import { useMessage } from '@/composables/useMessage';
import { useToast } from '@/composables/useToast';
import { useTelemetry } from '@/composables/useTelemetry';
import { useDocumentTitle } from '@/composables/useDocumentTitle';
import { useRootStore } from '@/stores/root.store';
const IdentityProviderSettingsType = {
@ -21,6 +22,7 @@ const ssoStore = useSSOStore();
const uiStore = useUIStore();
const message = useMessage();
const toast = useToast();
const documentTitle = useDocumentTitle();
const ssoActivatedLabel = computed(() =>
ssoStore.isSamlLoginEnabled
@ -144,6 +146,7 @@ const isToggleSsoDisabled = computed(() => {
});
onMounted(async () => {
documentTitle.set(i18n.baseText('settings.sso.title'));
if (!ssoStore.isEnterpriseSamlEnabled) {
return;
}

View File

@ -7,6 +7,7 @@ import { telemetry } from '@/plugins/telemetry';
import { i18n as locale } from '@/plugins/i18n';
import { useUIStore } from '@/stores/ui.store';
import { useToast } from '@/composables/useToast';
import { useDocumentTitle } from '@/composables/useDocumentTitle';
import { hasPermission } from '@/utils/rbac/permissions';
const usageStore = useUsageStore();
@ -14,6 +15,7 @@ const route = useRoute();
const router = useRouter();
const uiStore = useUIStore();
const toast = useToast();
const documentTitle = useDocumentTitle();
const queryParamCallback = ref<string>(
`callback=${encodeURIComponent(`${window.location.origin}${window.location.pathname}`)}`,
@ -64,6 +66,7 @@ const onLicenseActivation = async () => {
};
onMounted(async () => {
documentTitle.set(locale.baseText('settings.usageAndPlan.title'));
usageStore.setLoading(true);
if (route.query.key) {
try {

View File

@ -12,6 +12,7 @@ import { useClipboard } from '@/composables/useClipboard';
import type { UpdateGlobalRolePayload } from '@/api/users';
import { computed, onMounted } from 'vue';
import { useI18n } from '@/composables/useI18n';
import { useDocumentTitle } from '@/composables/useDocumentTitle';
const clipboard = useClipboard();
const { showToast, showError } = useToast();
@ -20,6 +21,7 @@ const settingsStore = useSettingsStore();
const uiStore = useUIStore();
const usersStore = useUsersStore();
const ssoStore = useSSOStore();
const documentTitle = useDocumentTitle();
const i18n = useI18n();
@ -28,6 +30,8 @@ const showUMSetupWarning = computed(() => {
});
onMounted(async () => {
documentTitle.set(i18n.baseText('settings.users'));
if (!showUMSetupWarning.value) {
await usersStore.fetchUsers();
}

View File

@ -13,7 +13,7 @@ import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { isFullTemplatesCollection } from '@/utils/templates/typeGuards';
import { useRoute, useRouter } from 'vue-router';
import { useTelemetry } from '@/composables/useTelemetry';
import { setPageTitle } from '@/utils/htmlUtils';
import { useDocumentTitle } from '@/composables/useDocumentTitle';
import { useI18n } from '@/composables/useI18n';
const externalHooks = useExternalHooks();
@ -25,6 +25,7 @@ const route = useRoute();
const router = useRouter();
const telemetry = useTelemetry();
const i18n = useI18n();
const documentTitle = useDocumentTitle();
const loading = ref(true);
const notFoundError = ref(false);
@ -89,9 +90,9 @@ watch(
() => collection.value,
() => {
if (collection.value && 'full' in collection.value && collection.value.full) {
setPageTitle(`n8n - Template collection: ${collection.value.name}`);
documentTitle.set(`Template collection: ${collection.value.name}`);
} else {
setPageTitle('n8n - Templates');
documentTitle.set('Templates');
}
},
);

View File

@ -13,7 +13,6 @@ import type {
ITemplatesCategory,
} from '@/Interface';
import type { IDataObject } from 'n8n-workflow';
import { setPageTitle } from '@/utils/htmlUtils';
import { CREATOR_HUB_URL, VIEWS } from '@/constants';
import { useSettingsStore } from '@/stores/settings.store';
import { useUsersStore } from '@/stores/users.store';
@ -22,6 +21,7 @@ import { useUIStore } from '@/stores/ui.store';
import { useToast } from '@/composables/useToast';
import { usePostHog } from '@/stores/posthog.store';
import { useDebounce } from '@/composables/useDebounce';
import { useDocumentTitle } from '@/composables/useDocumentTitle';
interface ISearchEvent {
search_string: string;
@ -55,6 +55,7 @@ export default defineComponent({
return {
callDebounced,
...useToast(),
documentTitle: useDocumentTitle(),
};
},
data() {
@ -116,7 +117,7 @@ export default defineComponent({
},
},
async mounted() {
setPageTitle('n8n - Templates');
this.documentTitle.set('Templates');
await this.loadCategories();
void this.loadWorkflowsAndCollections(true);
void this.usersStore.showPersonalizationSurvey();

View File

@ -1,6 +1,5 @@
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue';
import { setPageTitle } from '@/utils/htmlUtils';
import { useTemplatesStore } from '@/stores/templates.store';
import { usePostHog } from '@/stores/posthog.store';
import { useTemplateWorkflow } from '@/utils/templates/templateActions';
@ -8,6 +7,7 @@ import { useExternalHooks } from '@/composables/useExternalHooks';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useRoute, useRouter } from 'vue-router';
import { useTelemetry } from '@/composables/useTelemetry';
import { useDocumentTitle } from '@/composables/useDocumentTitle';
import { useI18n } from '@/composables/useI18n';
import TemplatesView from './TemplatesView.vue';
@ -20,6 +20,7 @@ const route = useRoute();
const router = useRouter();
const telemetry = useTelemetry();
const i18n = useI18n();
const documentTitle = useDocumentTitle();
const loading = ref(true);
const showPreview = ref(true);
@ -63,9 +64,9 @@ watch(
() => template.value,
(newTemplate) => {
if (newTemplate) {
setPageTitle(`n8n - Template template: ${newTemplate.name}`);
documentTitle.set(`Template template: ${newTemplate.name}`);
} else {
setPageTitle('n8n - Templates');
documentTitle.set('Templates');
}
},
);

View File

@ -1,5 +1,5 @@
<script lang="ts" setup>
import { computed, ref, onBeforeMount, onBeforeUnmount } from 'vue';
import { computed, ref, onBeforeMount, onBeforeUnmount, onMounted } from 'vue';
import { useEnvironmentsStore } from '@/stores/environments.ee.store';
import { useSettingsStore } from '@/stores/settings.store';
import { useSourceControlStore } from '@/stores/sourceControl.store';
@ -9,6 +9,7 @@ import { useI18n } from '@/composables/useI18n';
import { useTelemetry } from '@/composables/useTelemetry';
import { useToast } from '@/composables/useToast';
import { useMessage } from '@/composables/useMessage';
import { useDocumentTitle } from '@/composables/useDocumentTitle';
import type { IResource } from '@/components/layouts/ResourcesListLayout.vue';
import ResourcesListLayout from '@/components/layouts/ResourcesListLayout.vue';
@ -28,6 +29,7 @@ const telemetry = useTelemetry();
const i18n = useI18n();
const message = useMessage();
const sourceControlStore = useSourceControlStore();
const documentTitle = useDocumentTitle();
let sourceControlStoreUnsubscribe = () => {};
const layoutRef = ref<InstanceType<typeof ResourcesListLayout> | null>(null);
@ -248,6 +250,10 @@ onBeforeMount(() => {
onBeforeUnmount(() => {
sourceControlStoreUnsubscribe();
});
onMounted(() => {
documentTitle.set(i18n.baseText('variables.heading'));
});
</script>
<template>

View File

@ -17,6 +17,7 @@ import ProjectTabs from '@/components/Projects/ProjectTabs.vue';
import { useTemplatesStore } from '@/stores/templates.store';
import { getResourcePermissions } from '@/permissions';
import { usePostHog } from '@/stores/posthog.store';
import { useDocumentTitle } from '@/composables/useDocumentTitle';
interface Filters {
search: string;
@ -49,6 +50,7 @@ const WorkflowsView = defineComponent({
} as Filters,
sourceControlStoreUnsubscribe: () => {},
loading: false,
documentTitle: useDocumentTitle(),
};
},
computed: {
@ -149,6 +151,7 @@ const WorkflowsView = defineComponent({
},
},
async mounted() {
this.documentTitle.set(this.$locale.baseText('workflows.heading'));
await this.setFiltersFromQueryString();
void this.usersStore.showPersonalizationSurvey();