diff --git a/backend/i18n/frontend_en.json b/backend/i18n/frontend_en.json index 943ef92b5..124cf6711 100644 --- a/backend/i18n/frontend_en.json +++ b/backend/i18n/frontend_en.json @@ -287,6 +287,7 @@ "common.httpConflict": "Failed to make the update. Another user has made a change. Please reload.", "common.httpLimit": "You have exceeded the maximum limit of API calls.", "common.id": "Identity", + "common.in": "in", "common.label": "Label", "common.language": "Language", "common.languages": "Languages", diff --git a/backend/i18n/frontend_it.json b/backend/i18n/frontend_it.json index ab030ba65..097f3494a 100644 --- a/backend/i18n/frontend_it.json +++ b/backend/i18n/frontend_it.json @@ -287,6 +287,7 @@ "common.httpConflict": "Non è stato possibile effettuare l'aggiornamento. Un altro utente ha fatto delle modifiche. Per favore ricarica.", "common.httpLimit": "Hai superato il limite massimo di chiamate API.", "common.id": "Identificativo", + "common.in": "in", "common.label": "Etichetta", "common.language": "Lingua", "common.languages": "Lingue", diff --git a/backend/i18n/frontend_nl.json b/backend/i18n/frontend_nl.json index 3563f6f1e..21744908b 100644 --- a/backend/i18n/frontend_nl.json +++ b/backend/i18n/frontend_nl.json @@ -287,6 +287,7 @@ "common.httpConflict": "De update is mislukt. Een andere gebruiker heeft een wijziging aangebracht. Laad opnieuw.", "common.httpLimit": "Je hebt de maximale limiet van API-aanroepen overschreden.", "common.id": "Identity", + "common.in": "in", "common.label": "Label", "common.language": "Language", "common.languages": "Talen", diff --git a/backend/i18n/frontend_zh.json b/backend/i18n/frontend_zh.json index 3528125c6..052bab327 100644 --- a/backend/i18n/frontend_zh.json +++ b/backend/i18n/frontend_zh.json @@ -287,6 +287,7 @@ "common.httpConflict": "更新失败。其他用户进行了更改。请重新加载。", "common.httpLimit": "您已超出 API 调用的最大限制。", "common.id": "身份", + "common.in": "in", "common.label": "标签", "common.language": "语言", "common.languages": "语言", diff --git a/backend/i18n/source/frontend_en.json b/backend/i18n/source/frontend_en.json index 943ef92b5..124cf6711 100644 --- a/backend/i18n/source/frontend_en.json +++ b/backend/i18n/source/frontend_en.json @@ -287,6 +287,7 @@ "common.httpConflict": "Failed to make the update. Another user has made a change. Please reload.", "common.httpLimit": "You have exceeded the maximum limit of API calls.", "common.id": "Identity", + "common.in": "in", "common.label": "Label", "common.language": "Language", "common.languages": "Languages", diff --git a/frontend/app/framework/angular/forms/indeterminate-value.directive.ts b/frontend/app/framework/angular/forms/indeterminate-value.directive.ts index 253f5b5a2..2e99acd1f 100644 --- a/frontend/app/framework/angular/forms/indeterminate-value.directive.ts +++ b/frontend/app/framework/angular/forms/indeterminate-value.directive.ts @@ -71,7 +71,7 @@ export class IndeterminateValueDirective implements ControlValueAccessor { this.isChecked = obj; } - public setDisabledState(isDisabled: boolean): void { + public setDisabledState(isDisabled: boolean) { this.renderer.setProperty(this.element.nativeElement, 'disabled', isDisabled); } diff --git a/frontend/app/framework/angular/forms/transform-input.directive.ts b/frontend/app/framework/angular/forms/transform-input.directive.ts index e4d9f2a9e..370d41709 100644 --- a/frontend/app/framework/angular/forms/transform-input.directive.ts +++ b/frontend/app/framework/angular/forms/transform-input.directive.ts @@ -76,7 +76,7 @@ export class TransformInputDirective implements ControlValueAccessor { this.renderer.setProperty(this.element.nativeElement, 'value', normalizedValue); } - public setDisabledState(isDisabled: boolean): void { + public setDisabledState(isDisabled: boolean) { this.renderer.setProperty(this.element.nativeElement, 'disabled', isDisabled); } diff --git a/frontend/app/framework/angular/stateful.component.ts b/frontend/app/framework/angular/stateful.component.ts index a3b8f60a8..aeb48a0f3 100644 --- a/frontend/app/framework/angular/stateful.component.ts +++ b/frontend/app/framework/angular/stateful.component.ts @@ -115,7 +115,7 @@ export abstract class StatefulControlComponent extends Sta this.fnChanged(value); } - public setDisabledState(isDisabled: boolean): void { + public setDisabledState(isDisabled: boolean) { this.next({ isDisabled } as any); this.onDisabled(this.snapshot.isDisabled); diff --git a/frontend/app/framework/angular/video-player.component.ts b/frontend/app/framework/angular/video-player.component.ts index 5bb904b31..61f85087e 100644 --- a/frontend/app/framework/angular/video-player.component.ts +++ b/frontend/app/framework/angular/video-player.component.ts @@ -48,7 +48,7 @@ export class VideoPlayerComponent implements AfterViewInit, OnDestroy, OnChanges } } - public ngAfterViewInit(): void { + public ngAfterViewInit() { Promise.all([ this.resourceLoader.loadLocalScript('dependencies/videojs/video.min.js'), this.resourceLoader.loadLocalStyle('dependencies/videojs/video-js.min.css'), diff --git a/frontend/app/framework/services/message-bus.service.ts b/frontend/app/framework/services/message-bus.service.ts index 22d1aad41..108f4dee2 100644 --- a/frontend/app/framework/services/message-bus.service.ts +++ b/frontend/app/framework/services/message-bus.service.ts @@ -21,7 +21,7 @@ interface Message { export class MessageBus { private message$ = new Subject(); - public emit(data: T): void { + public emit(data: T) { const channel = ((data)['constructor']).name; this.message$.next({ channel, data }); diff --git a/frontend/app/shared/components/assets/asset-folder-dropdown-item.component.html b/frontend/app/shared/components/assets/asset-folder-dropdown-item.component.html new file mode 100644 index 000000000..cadceb56c --- /dev/null +++ b/frontend/app/shared/components/assets/asset-folder-dropdown-item.component.html @@ -0,0 +1,27 @@ +
+ + + + + + + + + +
+ {{node.item.folderName | sqxTranslate}} +
+
+ +
+ + +
\ No newline at end of file diff --git a/frontend/app/shared/components/assets/asset-folder-dropdown-item.component.scss b/frontend/app/shared/components/assets/asset-folder-dropdown-item.component.scss new file mode 100644 index 000000000..21a5b372e --- /dev/null +++ b/frontend/app/shared/components/assets/asset-folder-dropdown-item.component.scss @@ -0,0 +1,16 @@ +.loader { + font-size: 60%; + flex-grow: 0; + flex-shrink: 0; + margin-right: .25rem; +} + +.btn { + padding-bottom: 0; + padding-left: 0; + padding-right: 0; + flex-grow: 0; + flex-shrink: 0; + width: 1.5rem; + text-align: center; +} \ No newline at end of file diff --git a/frontend/app/shared/components/assets/asset-folder-dropdown-item.component.ts b/frontend/app/shared/components/assets/asset-folder-dropdown-item.component.ts new file mode 100644 index 000000000..673793156 --- /dev/null +++ b/frontend/app/shared/components/assets/asset-folder-dropdown-item.component.ts @@ -0,0 +1,92 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { AssetsService } from '@app/shared/internal'; +import { AssetFolderDropdowNode } from './asset-folder-dropdown.state'; + +@Component({ + selector: 'sqx-asset-folder-dropdown-item[appName][node]', + styleUrls: ['./asset-folder-dropdown-item.component.scss'], + templateUrl: './asset-folder-dropdown-item.component.html', +}) +export class AssetFolderDropdownItemComponent { + @Input() + public appName: string; + + @Input() + public node: AssetFolderDropdowNode; + + @Input() + public nodeLevel = 0; + + @Output() + public selectNode = new EventEmitter(); + + public get style() { + return { paddingLeft: `${this.nodeLevel}rem` }; + } + + constructor( + private readonly assetsService: AssetsService, + ) { + } + + public toggle() { + if (this.node.isExpanded && this.node.isLoaded) { + this.collapse(); + } else { + this.expand(); + } + } + + public collapse() { + this.node.isExpanded = false; + } + + public expand() { + this.node.isExpanded = true; + + this.loadChildren(); + } + + public loadChildren() { + if (this.node.isLoading || this.node.isLoaded) { + return; + } + + this.node.isLoading = true; + + this.assetsService.getAssetFolders(this.appName, this.node.item.id) + .subscribe({ + next: dto => { + if (dto.items.length > 0) { + const parent = this.node; + + for (const item of dto.items) { + if (!parent.children.find(x => x.item.id === item.id)) { + parent.children.push({ item, children: [], parent }); + } + } + + parent.children.sortByString(x => x.item.folderName); + } + + this.node.isLoaded = true; + }, + complete: () => { + setTimeout(() => { + this.node.isLoading = false; + }, 250); + }, + }); + } + + public trackByNode(_index: number, node: AssetFolderDropdowNode) { + return node.item.id; + } +} diff --git a/frontend/app/shared/components/assets/asset-folder-dropdown.component.html b/frontend/app/shared/components/assets/asset-folder-dropdown.component.html index 68b80ad87..8e7e844b6 100644 --- a/frontend/app/shared/components/assets/asset-folder-dropdown.component.html +++ b/frontend/app/shared/components/assets/asset-folder-dropdown.component.html @@ -1,5 +1,23 @@ - - - {{assetFolder.folderName | sqxTranslate | sqxHighlight:context}} - - \ No newline at end of file +
+
+
{{selection.item.folderName | sqxTranslate}} + + + {{ 'i18n:common.in' | sqxTranslate}} ./{{selectionPath}} + +
+
+
+ +
+ +
+ + +
+
+
\ No newline at end of file diff --git a/frontend/app/shared/components/assets/asset-folder-dropdown.component.scss b/frontend/app/shared/components/assets/asset-folder-dropdown.component.scss index e69de29bb..10452814d 100644 --- a/frontend/app/shared/components/assets/asset-folder-dropdown.component.scss +++ b/frontend/app/shared/components/assets/asset-folder-dropdown.component.scss @@ -0,0 +1,3 @@ +.control-dropdown { + max-width: 30rem; +} \ No newline at end of file diff --git a/frontend/app/shared/components/assets/asset-folder-dropdown.component.ts b/frontend/app/shared/components/assets/asset-folder-dropdown.component.ts index 4e8add19a..dd58ebcf9 100644 --- a/frontend/app/shared/components/assets/asset-folder-dropdown.component.ts +++ b/frontend/app/shared/components/assets/asset-folder-dropdown.component.ts @@ -5,22 +5,16 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, Input, OnInit } from '@angular/core'; -import { FormControl, NG_VALUE_ACCESSOR } from '@angular/forms'; -import { MathHelper, StatefulControlComponent, value$ } from '@app/framework'; -import { AssetPathItem, AssetsService } from '@app/shared/internal'; -import { AppsState } from '@app/shared/state/apps.state'; -import { ROOT_ITEM } from '@app/shared/state/assets.state'; +import { ChangeDetectorRef, Component, forwardRef, Input } from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; +import { ModalModel, StatefulControlComponent, Types } from '@app/framework'; +import { AppsState, AssetsService, ROOT_ITEM } from '@app/shared/internal'; +import { AssetFolderDropdowNode } from './asset-folder-dropdown.state'; export const SQX_ASSETS_FOLDER_DROPDOWN_CONTROL_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => AssetFolderDropdownComponent), multi: true, }; -interface State { - // The asset folders. - assetFolders: ReadonlyArray; -} - @Component({ selector: 'sqx-asset-folder-dropdown', styleUrls: ['./asset-folder-dropdown.component.scss'], @@ -28,52 +22,118 @@ interface State { providers: [ SQX_ASSETS_FOLDER_DROPDOWN_CONTROL_VALUE_ACCESSOR, ], - changeDetection: ChangeDetectionStrategy.OnPush, }) -export class AssetFolderDropdownComponent extends StatefulControlComponent implements OnInit { +export class AssetFolderDropdownComponent extends StatefulControlComponent { @Input() public set disabled(value: boolean | undefined | null) { this.setDisabledState(value === true); } - public control = new FormControl(); + public get appName() { + return this.appsState.appName; + } + + public root: AssetFolderDropdowNode = { item: ROOT_ITEM, children: [], parent: null }; + + public selection = this.root; + public selectionPath: string; + + public dropdown = new ModalModel(); constructor(changeDetector: ChangeDetectorRef, private readonly appsState: AppsState, private readonly assetsService: AssetsService, ) { - super(changeDetector, { - assetFolders: [], - }); - - this.own( - value$(this.control) - .subscribe((value: any) => { - if (this.control.enabled) { - this.callChange(value); - this.callTouched(); - } - })); + super(changeDetector, {}); } - public ngOnInit() { - this.assetsService.getAssetFolders(this.appsState.appName, MathHelper.EMPTY_GUID) + public writeValue(obj: string) { + if (!Types.isString(obj)) { + obj = ROOT_ITEM.id; + } + + const node = this.findNode(this.root, obj); + + if (node?.isLoaded) { + this.select(node, false); + return; + } + + this.assetsService.getAssetFolders(this.appName, obj) .subscribe(dto => { - const assetFolders = [ROOT_ITEM, ...dto.items]; + let parent = this.root; + + for (const item of dto.path) { + let newParent = parent.children.find(x => x.item.id === item.id); + + if (!newParent) { + newParent = { item, children: [], parent }; + parent.children.push(newParent); + parent.children.sortByString(x => x.item.folderName); + } + + parent = newParent; + } + + if (dto.items.length > 0) { + for (const item of dto.items) { + if (!parent.children.find(x => x.item.id === item.id)) { + parent.children.push({ item, children: [], parent }); + } + } - this.next({ assetFolders }); + parent.children.sortByString(x => x.item.folderName); + } + + this.select(parent, false); }); } - public onDisabled(isDisabled: boolean) { - if (isDisabled) { - this.control.disable({ emitEvent: false }); - } else { - this.control.enable({ emitEvent: false }); + public select(selected: AssetFolderDropdowNode, emit = true) { + this.resetSelected(this.root); + + const path: AssetFolderDropdowNode[] = []; + + let current: AssetFolderDropdowNode | null = selected.parent; + + while (current) { + path.push(current); + + current.isExpanded = true; + current = current.parent; + } + + this.selection = selected; + this.selection.isSelected = true; + this.selectionPath = path.filter(x => x.item !== ROOT_ITEM).map(x => x.item.folderName).join('/'); + + if (emit) { + this.callChange(selected.item.id); + this.callTouched(); + + this.dropdown.hide(); } } - public writeValue(obj: any): void { - this.control.setValue(obj || ROOT_ITEM.id); + private resetSelected(node: AssetFolderDropdowNode) { + node.isSelected = false; + + for (const child of node.children) { + this.resetSelected(child); + } + } + + private findNode(node: AssetFolderDropdowNode, id: string) { + if (node.item.id === id) { + return node; + } + + for (const child of node.children) { + if (this.findNode(child, id)) { + return child; + } + } + + return undefined; } } diff --git a/frontend/app/shared/components/assets/asset-folder-dropdown.state.ts b/frontend/app/shared/components/assets/asset-folder-dropdown.state.ts new file mode 100644 index 000000000..640cb9f69 --- /dev/null +++ b/frontend/app/shared/components/assets/asset-folder-dropdown.state.ts @@ -0,0 +1,31 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { AssetPathItem } from '@app/shared/internal'; + +export interface AssetFolderDropdowNode { + // The child folders. + children: AssetFolderDropdowNode[]; + + // The parent folder. + parent: AssetFolderDropdowNode | null; + + // True if selected. + isSelected?: boolean; + + // True if loading + isLoading?: boolean; + + // True if loaded + isLoaded?: boolean; + + // True if expanded + isExpanded?: boolean; + + // The folder. + item: AssetPathItem; +} diff --git a/frontend/app/shared/components/forms/geolocation-editor.component.ts b/frontend/app/shared/components/forms/geolocation-editor.component.ts index 733d5bfe2..8ee091fb1 100644 --- a/frontend/app/shared/components/forms/geolocation-editor.component.ts +++ b/frontend/app/shared/components/forms/geolocation-editor.component.ts @@ -120,7 +120,7 @@ export class GeolocationEditorComponent extends StatefulControlComponent any = isDisabled ? x => x.disable() : @@ -139,7 +139,7 @@ export class GeolocationEditorComponent extends StatefulControlComponent
-
diff --git a/frontend/app/shared/declarations.ts b/frontend/app/shared/declarations.ts index 4a63a8c6b..cd870b884 100644 --- a/frontend/app/shared/declarations.ts +++ b/frontend/app/shared/declarations.ts @@ -9,6 +9,7 @@ export * from './components/app-form.component'; export * from './components/assets/asset-dialog.component'; export * from './components/assets/asset-folder-dialog.component'; export * from './components/assets/asset-folder-dropdown.component'; +export * from './components/assets/asset-folder-dropdown-item.component'; export * from './components/assets/asset-folder.component'; export * from './components/assets/asset-history.component'; export * from './components/assets/asset-path.component'; diff --git a/frontend/app/shared/module.ts b/frontend/app/shared/module.ts index 39158fc7c..1585b0c2f 100644 --- a/frontend/app/shared/module.ts +++ b/frontend/app/shared/module.ts @@ -12,7 +12,7 @@ import { RouterModule } from '@angular/router'; import { SqxFrameworkModule } from '@app/framework'; import { MentionModule } from 'angular-mentions'; import { NgxDocViewerModule } from 'ngx-doc-viewer'; -import { AppFormComponent, AppLanguagesService, AppMustExistGuard, AppsService, AppsState, AssetComponent, AssetDialogComponent, AssetFolderComponent, AssetFolderDialogComponent, AssetFolderDropdownComponent, AssetHistoryComponent, AssetPathComponent, AssetPreviewUrlPipe, AssetsListComponent, AssetsSelectorComponent, AssetsService, AssetsState, AssetTextEditorComponent, AssetUploaderComponent, AssetUploaderState, AssetUrlPipe, AuthInterceptor, AuthService, AutoSaveService, BackupsService, BackupsState, ClientsService, ClientsState, CommentComponent, CommentsComponent, CommentsService, ContentListCellDirective, ContentListFieldComponent, ContentListHeaderComponent, ContentListWidthPipe, ContentMustExistGuard, ContentsColumnsPipe, ContentSelectorComponent, ContentSelectorItemComponent, ContentsService, ContentsState, ContentStatusComponent, ContentValueComponent, ContentValueEditorComponent, ContributorsService, ContributorsState, FileIconPipe, FilterComparisonComponent, FilterLogicalComponent, FilterNodeComponent, GeolocationEditorComponent, GraphQlService, HelpComponent, HelpMarkdownPipe, HelpService, HistoryComponent, HistoryListComponent, HistoryMessagePipe, HistoryService, ImageCropperComponent, ImageFocusPointComponent, LanguagesService, LanguagesState, LoadAppsGuard, LoadLanguagesGuard, LoadSchemasGuard, MarkdownEditorComponent, MustBeAuthenticatedGuard, MustBeNotAuthenticatedGuard, NewsService, NotifoComponent, PlansService, PlansState, PreviewableType, QueryComponent, QueryListComponent, QueryPathComponent, ReferenceDropdownComponent, ReferenceInputComponent, ReferencesCheckboxesComponent, ReferencesTagsComponent, RichEditorComponent, RolesService, RolesState, RuleEventsState, RuleMustExistGuard, RuleSimulatorState, RulesService, RulesState, SavedQueriesComponent, SchemaCategoryComponent, SchemaMustExistGuard, SchemaMustExistPublishedGuard, SchemaMustNotBeSingletonGuard, SchemasService, SchemasState, SchemaTagSource, SearchFormComponent, SearchService, SortingComponent, StockPhotoService, TableHeaderComponent, TranslationsService, UIService, UIState, UnsetAppGuard, UsagesService, UserDtoPicture, UserIdPicturePipe, UserNamePipe, UserNameRefPipe, UserPicturePipe, UserPictureRefPipe, UsersProviderService, UsersService, WorkflowsService, WorkflowsState } from './declarations'; +import { AppFormComponent, AppLanguagesService, AppMustExistGuard, AppsService, AppsState, AssetComponent, AssetDialogComponent, AssetFolderComponent, AssetFolderDialogComponent, AssetFolderDropdownComponent, AssetFolderDropdownItemComponent, AssetHistoryComponent, AssetPathComponent, AssetPreviewUrlPipe, AssetsListComponent, AssetsSelectorComponent, AssetsService, AssetsState, AssetTextEditorComponent, AssetUploaderComponent, AssetUploaderState, AssetUrlPipe, AuthInterceptor, AuthService, AutoSaveService, BackupsService, BackupsState, ClientsService, ClientsState, CommentComponent, CommentsComponent, CommentsService, ContentListCellDirective, ContentListFieldComponent, ContentListHeaderComponent, ContentListWidthPipe, ContentMustExistGuard, ContentsColumnsPipe, ContentSelectorComponent, ContentSelectorItemComponent, ContentsService, ContentsState, ContentStatusComponent, ContentValueComponent, ContentValueEditorComponent, ContributorsService, ContributorsState, FileIconPipe, FilterComparisonComponent, FilterLogicalComponent, FilterNodeComponent, GeolocationEditorComponent, GraphQlService, HelpComponent, HelpMarkdownPipe, HelpService, HistoryComponent, HistoryListComponent, HistoryMessagePipe, HistoryService, ImageCropperComponent, ImageFocusPointComponent, LanguagesService, LanguagesState, LoadAppsGuard, LoadLanguagesGuard, LoadSchemasGuard, MarkdownEditorComponent, MustBeAuthenticatedGuard, MustBeNotAuthenticatedGuard, NewsService, NotifoComponent, PlansService, PlansState, PreviewableType, QueryComponent, QueryListComponent, QueryPathComponent, ReferenceDropdownComponent, ReferenceInputComponent, ReferencesCheckboxesComponent, ReferencesTagsComponent, RichEditorComponent, RolesService, RolesState, RuleEventsState, RuleMustExistGuard, RuleSimulatorState, RulesService, RulesState, SavedQueriesComponent, SchemaCategoryComponent, SchemaMustExistGuard, SchemaMustExistPublishedGuard, SchemaMustNotBeSingletonGuard, SchemasService, SchemasState, SchemaTagSource, SearchFormComponent, SearchService, SortingComponent, StockPhotoService, TableHeaderComponent, TranslationsService, UIService, UIState, UnsetAppGuard, UsagesService, UserDtoPicture, UserIdPicturePipe, UserNamePipe, UserNameRefPipe, UserPicturePipe, UserPictureRefPipe, UsersProviderService, UsersService, WorkflowsService, WorkflowsState } from './declarations'; @NgModule({ imports: [ @@ -29,6 +29,7 @@ import { AppFormComponent, AppLanguagesService, AppMustExistGuard, AppsService, AssetFolderComponent, AssetFolderDialogComponent, AssetFolderDropdownComponent, + AssetFolderDropdownItemComponent, AssetHistoryComponent, AssetPathComponent, AssetPreviewUrlPipe, diff --git a/frontend/app/theme/_bootstrap.scss b/frontend/app/theme/_bootstrap.scss index bd9f2b41c..b368ec8bb 100644 --- a/frontend/app/theme/_bootstrap.scss +++ b/frontend/app/theme/_bootstrap.scss @@ -318,7 +318,6 @@ a { &:hover { color: $color-text; - } } @@ -469,6 +468,17 @@ a { &-outline-secondary { color: $color-text-decent; } + + &-decent { + &:focus { + outline: none !important; + } + + &:active, + &:focus { + box-shadow: none !important; + } + } } $icon-size: 4.5rem; diff --git a/frontend/app/theme/_forms.scss b/frontend/app/theme/_forms.scss index 1ab8fc865..9ff1fc145 100644 --- a/frontend/app/theme/_forms.scss +++ b/frontend/app/theme/_forms.scss @@ -180,6 +180,10 @@ .text-muted { color: $color-white !important; } + + i { + color: $color-white !important; + } } &:active,