@@ -32,12 +32,12 @@ import {
3232 N8nTooltip ,
3333 type IMenuItem ,
3434} from ' @n8n/design-system' ;
35- import ProjectSharing from ' @/features/collaboration/projects/components/ProjectSharing.vue' ;
3635import { useProjectsStore } from ' @/features/collaboration/projects/projects.store' ;
37- import type { ProjectSharingData } from ' @/features/collaboration/projects/projects. types' ;
36+ import type { IconOrEmoji } from ' @n8n/design-system/components/N8nIconPicker/ types' ;
3837import { useUIStore } from ' @/app/stores/ui.store' ;
3938import { useEnvFeatureFlag } from ' @/features/shared/envFeatureFlag/useEnvFeatureFlag' ;
4039import Banner from ' @/app/components/Banner.vue' ;
40+ import type { ProjectSharingData } from ' @/features/collaboration/projects/projects.types' ;
4141
4242// Props
4343const props = withDefaults (
@@ -116,20 +116,49 @@ const sidebarItems = computed(() => {
116116 return menuItems ;
117117});
118118
119- const scopeProjects = computed (() =>
120- projectsStore .teamProjects .filter (
121- (p : ProjectSharingData ) => ! modal .projectIds .value .includes (p .id ),
122- ),
119+ const scopeOptions = computed <Array <{ value: string ; label: string ; icon: IconOrEmoji }>>(() => {
120+ const options: Array <{ value: string ; label: string ; icon: IconOrEmoji }> = [
121+ {
122+ value: ' ' ,
123+ label: i18n .baseText (' settings.secretsProviderConnections.modal.scope.global' ),
124+ icon: { type: ' icon' , value: ' globe' },
125+ },
126+ ];
127+
128+ options .push (
129+ ... projectsStore .teamProjects .map ((project : ProjectSharingData ) => {
130+ const icon = (project .icon ?? {
131+ type: ' icon' as const ,
132+ value: ' layer-group' ,
133+ }) as IconOrEmoji ;
134+ return {
135+ value: project .id ,
136+ label: project .name ?? project .id ,
137+ icon ,
138+ };
139+ }),
140+ );
141+
142+ return options ;
143+ });
144+
145+ const scopeSelectValue = computed (() =>
146+ modal .isSharedGlobally .value ? ' ' : (modal .projectIds .value [0 ] ?? ' ' ),
123147);
124148
125- // Sync scope changes to composable (max 1 project)
126- function handleScopeUpdate(value : ProjectSharingData [] | ProjectSharingData | null ) {
127- const project = Array .isArray (value ) ? value .at (- 1 ) : value ;
128- modal .setScopeState (project ? [project .id ] : [], false );
129- }
149+ const selectedScopeIcon = computed <IconOrEmoji >(() => {
150+ const selectedOption = scopeOptions .value .find (
151+ (option ) => option .value === scopeSelectValue .value ,
152+ );
153+ return selectedOption ?.icon ?? { type: ' icon' as const , value: ' globe' };
154+ });
130155
131- function handleShareGlobally(value : boolean ) {
132- modal .setScopeState ([], value );
156+ function handleScopeSelect(value : string ) {
157+ if (value === ' ' ) {
158+ modal .setScopeState ([], true );
159+ } else {
160+ modal .setScopeState ([value ], false );
161+ }
133162}
134163
135164// Handlers
@@ -423,29 +452,48 @@ onMounted(async () => {
423452 <N8nInfoTip :bold =" false" class =" mb-s" >
424453 {{ i18n.baseText('settings.secretsProviderConnections.modal.scope.info') }}
425454 </N8nInfoTip >
426- <ProjectSharing
427- :model-value =" modal.sharedWithProjects.value"
428- :projects =" scopeProjects"
429- :readonly =" !modal.canUpdate.value"
430- :static =" !modal.canUpdate.value"
431- :placeholder ="
432- i18n.baseText(
433- 'settings.secretsProviderConnections.modal.scope.placeholder.project',
434- )
435- "
436- :all-users-label ="
437- i18n.baseText('settings.secretsProviderConnections.modal.scope.global')
438- "
439- :empty-options-text ="
440- i18n.baseText(
441- 'settings.secretsProviderConnections.modal.scope.emptyOptionsText',
442- )
443- "
444- :can-share-globally =" modal.canShareGlobally.value"
445- :is-shared-globally =" modal.isSharedGlobally.value"
446- @update:share-with-all-users =" handleShareGlobally"
447- @update:model-value =" handleScopeUpdate"
448- />
455+ <N8nInputLabel
456+ :label =" i18n.baseText('settings.secretsProviderConnections.modal.scope.label')"
457+ >
458+ <N8nSelect
459+ :model-value =" scopeSelectValue"
460+ size =" large"
461+ filterable
462+ :disabled =" !modal.canUpdate.value"
463+ data-test-id =" secrets-provider-scope-select"
464+ @update:model-value =" handleScopeSelect"
465+ >
466+ <template #prefix >
467+ <N8nText
468+ v-if =" selectedScopeIcon?.type === 'emoji'"
469+ color =" text-light"
470+ :class =" $style.menuItemEmoji"
471+ >
472+ {{ selectedScopeIcon.value }}
473+ </N8nText >
474+ <N8nIcon
475+ v-else-if =" selectedScopeIcon?.value"
476+ color =" text-light"
477+ :icon =" selectedScopeIcon.value"
478+ />
479+ </template >
480+ <N8nOption
481+ v-for =" option in scopeOptions"
482+ :key =" option.value || 'global'"
483+ :value =" option.value"
484+ :label =" option.label"
485+ :class =" { [$style.globalOption]: option.value === '' }"
486+ >
487+ <div :class =" $style.optionContent" >
488+ <N8nText v-if =" option.icon?.type === 'emoji'" :class =" $style.menuItemEmoji" >
489+ {{ option.icon.value }}
490+ </N8nText >
491+ <N8nIcon v-else-if =" option.icon?.value" :icon =" option.icon.value" />
492+ <span >{{ option.label }}</span >
493+ </div >
494+ </N8nOption >
495+ </N8nSelect >
496+ </N8nInputLabel >
449497 </div >
450498 </div >
451499 </div >
@@ -562,4 +610,31 @@ onMounted(async () => {
562610 display : block ;
563611 margin-top : var (--spacing--4xs );
564612}
613+
614+ .optionContent {
615+ display : flex ;
616+ align-items : center ;
617+ gap : var (--spacing--2xs );
618+ }
619+
620+ .menuItemEmoji {
621+ font-size : var (--font-size--sm );
622+ line-height : 1 ;
623+ }
624+
625+ .globalOption {
626+ position : relative ;
627+ margin-bottom : var (--spacing--sm );
628+ overflow : visible ;
629+
630+ & ::after {
631+ content : ' ' ;
632+ position : absolute ;
633+ bottom : calc (var (--spacing--2xs ) * -1 );
634+ left : var (--spacing--xs );
635+ right : var (--spacing--xs );
636+ height : 1px ;
637+ background-color : var (--color--foreground );
638+ }
639+ }
565640 </style >
0 commit comments