@rancher/shell 3.0.5-rc.5 → 3.0.5-rc.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/data/aws-regions.json +1 -0
- package/assets/images/key.svg +17 -0
- package/assets/styles/base/_spacing.scss +2 -2
- package/assets/styles/global/_form.scss +1 -1
- package/assets/styles/global/_labeled-input.scss +1 -1
- package/assets/styles/themes/_dark.scss +3 -0
- package/assets/styles/themes/_light.scss +3 -0
- package/assets/styles/vendor/vue-select.scss +1 -1
- package/assets/translations/en-us.yaml +404 -64
- package/assets/translations/zh-hans.yaml +3 -4
- package/cloud-credential/gcp.vue +9 -1
- package/components/AppModal.vue +2 -0
- package/components/CodeMirror.vue +1 -1
- package/components/ConfigMapSettings/Settings.vue +377 -0
- package/components/ConfigMapSettings/index.vue +354 -0
- package/components/CruResource.vue +1 -2
- package/components/DetailText.vue +61 -11
- package/components/Drawer/Chrome.vue +116 -0
- package/components/Drawer/ResourceDetailDrawer/ConfigTab.vue +61 -0
- package/components/Drawer/ResourceDetailDrawer/YamlTab.vue +48 -0
- package/components/Drawer/ResourceDetailDrawer/__tests__/ConfigTab.test.ts +54 -0
- package/components/Drawer/ResourceDetailDrawer/__tests__/YamlTab.test.ts +80 -0
- package/components/Drawer/ResourceDetailDrawer/__tests__/composables.test.ts +82 -0
- package/components/Drawer/ResourceDetailDrawer/__tests__/helpers.test.ts +42 -0
- package/components/Drawer/ResourceDetailDrawer/composables.ts +50 -0
- package/components/Drawer/ResourceDetailDrawer/helpers.ts +10 -0
- package/components/Drawer/ResourceDetailDrawer/index.vue +110 -0
- package/components/GrowlManager.vue +16 -15
- package/components/IconOrSvg.vue +5 -0
- package/components/KeyValueView.vue +1 -1
- package/components/LocaleSelector.vue +9 -1
- package/components/ProgressBarMulti.vue +1 -0
- package/components/PromptModal.vue +6 -1
- package/components/RelatedResources.vue +4 -12
- package/components/Resource/Detail/Additional.vue +46 -0
- package/components/Resource/Detail/Metadata/Annotations/__tests__/index.test.ts +1 -1
- package/components/Resource/Detail/Metadata/Annotations/index.vue +5 -0
- package/components/Resource/Detail/Metadata/IdentifyingInformation/__tests__/identifying-fields.test.ts +223 -0
- package/components/Resource/Detail/Metadata/IdentifyingInformation/composable.ts +37 -254
- package/components/Resource/Detail/Metadata/IdentifyingInformation/identifying-fields.ts +298 -0
- package/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue +27 -5
- package/components/Resource/Detail/Metadata/KeyValue.vue +25 -17
- package/components/Resource/Detail/Metadata/Labels/__tests__/index.test.ts +1 -1
- package/components/Resource/Detail/Metadata/Labels/index.vue +4 -0
- package/components/Resource/Detail/Metadata/__tests__/KeyValue.test.ts +1 -1
- package/components/Resource/Detail/Metadata/__tests__/Rectangle.test.ts +1 -1
- package/components/Resource/Detail/Metadata/__tests__/composables.test.ts +75 -0
- package/components/Resource/Detail/Metadata/composables.ts +60 -11
- package/components/Resource/Detail/Metadata/index.vue +12 -5
- package/components/Resource/Detail/Page.vue +15 -0
- package/components/Resource/Detail/ResourceRow.vue +37 -18
- package/components/Resource/Detail/ResourceTabs/ConfigMapDataTab/__tests__/composables.test.ts +29 -0
- package/components/Resource/Detail/ResourceTabs/ConfigMapDataTab/__tests__/index.test.ts +48 -0
- package/components/Resource/Detail/ResourceTabs/ConfigMapDataTab/composables.ts +31 -0
- package/components/Resource/Detail/ResourceTabs/ConfigMapDataTab/index.vue +50 -0
- package/components/Resource/Detail/ResourceTabs/KnownHostsTab/__tests__/composables.test.ts +66 -0
- package/components/Resource/Detail/ResourceTabs/KnownHostsTab/composables.ts +21 -0
- package/components/Resource/Detail/ResourceTabs/KnownHostsTab/index.vue +31 -0
- package/components/Resource/Detail/ResourceTabs/SecretDataTab/Basic.vue +45 -0
- package/components/Resource/Detail/ResourceTabs/SecretDataTab/BasicAuth.vue +31 -0
- package/components/Resource/Detail/ResourceTabs/SecretDataTab/Certificate.vue +31 -0
- package/components/Resource/Detail/ResourceTabs/SecretDataTab/Registry.vue +22 -0
- package/components/Resource/Detail/ResourceTabs/SecretDataTab/ServiceAccountToken.vue +31 -0
- package/components/Resource/Detail/ResourceTabs/SecretDataTab/Ssh.vue +32 -0
- package/components/Resource/Detail/ResourceTabs/SecretDataTab/__tests__/Basic.test.ts +40 -0
- package/components/Resource/Detail/ResourceTabs/SecretDataTab/__tests__/BasicAuth.test.ts +33 -0
- package/components/Resource/Detail/ResourceTabs/SecretDataTab/__tests__/Certificate.test.ts +33 -0
- package/components/Resource/Detail/ResourceTabs/SecretDataTab/__tests__/Registry.test.ts +27 -0
- package/components/Resource/Detail/ResourceTabs/SecretDataTab/__tests__/ServiceAccountToken.test.ts +33 -0
- package/components/Resource/Detail/ResourceTabs/SecretDataTab/__tests__/Ssh.test.ts +33 -0
- package/components/Resource/Detail/ResourceTabs/SecretDataTab/__tests__/auth-types.test.ts +186 -0
- package/components/Resource/Detail/ResourceTabs/SecretDataTab/__tests__/composables.test.ts +102 -0
- package/components/Resource/Detail/ResourceTabs/SecretDataTab/auth-types.ts +109 -0
- package/components/Resource/Detail/ResourceTabs/SecretDataTab/composeables.ts +52 -0
- package/components/Resource/Detail/ResourceTabs/SecretDataTab/index.vue +71 -0
- package/components/Resource/Detail/TitleBar/Title.vue +2 -1
- package/components/Resource/Detail/TitleBar/__tests__/Title.test.ts +1 -1
- package/components/Resource/Detail/TitleBar/__tests__/Top.test.ts +1 -1
- package/components/Resource/Detail/TitleBar/__tests__/composables.test.ts +63 -0
- package/components/Resource/Detail/TitleBar/__tests__/index.test.ts +1 -1
- package/components/Resource/Detail/TitleBar/composables.ts +44 -0
- package/components/Resource/Detail/TitleBar/index.vue +83 -11
- package/components/Resource/Detail/composables.ts +45 -0
- package/components/ResourceDetail/Masthead/__tests__/index.test.ts +70 -0
- package/components/ResourceDetail/{__tests__/Masthead.test.ts → Masthead/__tests__/legacy.test.ts} +3 -3
- package/components/ResourceDetail/Masthead/index.vue +65 -0
- package/components/ResourceDetail/Masthead/latest.vue +44 -0
- package/components/ResourceDetail/__tests__/index.test.ts +26 -5
- package/components/ResourceDetail/index.vue +30 -16
- package/components/ResourceDetail/legacy.vue +18 -1
- package/components/ResourceList/Masthead.vue +6 -0
- package/components/ResourceYaml.vue +14 -1
- package/components/SlideInPanelManager.vue +46 -7
- package/components/StateDot/index.vue +28 -0
- package/components/Tabbed/index.vue +11 -15
- package/components/Wizard.vue +4 -2
- package/components/__tests__/ConfigMapSettings.test.ts +376 -0
- package/components/__tests__/GrowlManager.test.ts +0 -25
- package/components/auth/login/ldap.vue +1 -1
- package/components/fleet/FleetApplications.vue +0 -7
- package/components/fleet/FleetClusterTargets/TargetsList.vue +66 -0
- package/components/fleet/FleetClusterTargets/index.vue +455 -0
- package/components/fleet/FleetClusters.vue +25 -6
- package/components/fleet/FleetGitRepoPaths.vue +476 -0
- package/components/fleet/FleetHelmOps.vue +8 -0
- package/components/fleet/FleetRepos.vue +1 -6
- package/components/fleet/FleetResources.vue +4 -5
- package/components/fleet/FleetValuesFrom.vue +295 -0
- package/components/fleet/__tests__/FleetClusterTargets.test.ts +1224 -0
- package/components/fleet/__tests__/FleetGitRepoPaths.test.ts +265 -0
- package/components/fleet/__tests__/FleetOCIStorageSecret.test.ts +13 -13
- package/components/fleet/__tests__/FleetValuesFrom.test.ts +300 -0
- package/components/fleet/dashboard/ResourceCard.vue +1 -0
- package/components/fleet/dashboard/ResourceCardSummary.vue +1 -5
- package/components/fleet/dashboard/ResourceDetails.vue +8 -10
- package/components/fleet/dashboard/ResourcePanel.vue +15 -8
- package/components/form/ArrayList.vue +13 -2
- package/components/form/ChangePassword.vue +3 -1
- package/components/form/Footer.vue +10 -4
- package/components/form/KeyValue.vue +81 -43
- package/components/form/LabeledSelect.vue +56 -16
- package/components/form/Labels.vue +90 -17
- package/components/form/MatchExpressions.vue +46 -5
- package/components/form/NameNsDescription.vue +1 -1
- package/components/form/ResourceSelector.vue +1 -0
- package/components/form/ResourceTabs/index.vue +5 -0
- package/components/form/SecretSelector.vue +9 -2
- package/components/form/Select.vue +57 -19
- package/components/form/SimpleSecretSelector.vue +9 -2
- package/components/form/Taints.vue +21 -2
- package/components/form/UnitInput.vue +8 -0
- package/components/form/ValueFromResource.vue +1 -1
- package/components/form/__tests__/LabeledSelect.test.ts +8 -4
- package/components/form/__tests__/Labels.test.ts +360 -0
- package/components/form/__tests__/MatchExpressions.test.ts +16 -13
- package/components/form/__tests__/Select.test.ts +5 -2
- package/components/formatter/FleetApplicationSource.vue +1 -1
- package/components/formatter/WorkloadHealthScale.vue +1 -1
- package/components/google/AccountAccess.vue +211 -0
- package/components/google/types/gcp.d.ts +136 -0
- package/components/google/types/index.d.ts +101 -0
- package/components/google/util/__mocks__/gcp.ts +465 -0
- package/components/google/util/formatter.ts +82 -0
- package/components/google/util/gcp.ts +134 -0
- package/components/google/util/index.d.ts +11 -0
- package/components/nav/Favorite.vue +1 -1
- package/components/nav/Group.vue +70 -47
- package/components/nav/Header.vue +5 -1
- package/components/nav/NamespaceFilter.vue +13 -1
- package/components/nav/NotificationCenter/Notification.vue +510 -0
- package/components/nav/NotificationCenter/NotificationHeader.vue +112 -0
- package/components/nav/NotificationCenter/index.vue +148 -0
- package/composables/drawer.ts +26 -0
- package/composables/resources.test.ts +63 -0
- package/composables/resources.ts +38 -0
- package/composables/useIsNewDetailPageEnabled.ts +17 -0
- package/config/labels-annotations.js +6 -0
- package/config/product/auth.js +16 -1
- package/config/product/{cis.js → compliance.js} +23 -26
- package/config/product/explorer.js +5 -1
- package/config/product/fleet.js +7 -0
- package/config/product/settings.js +22 -11
- package/config/query-params.js +3 -0
- package/config/roles.ts +1 -1
- package/config/router/navigation-guards/authentication.js +51 -2
- package/config/router/routes.js +27 -31
- package/config/settings.ts +21 -3
- package/config/store.js +2 -0
- package/config/system-namespaces.js +1 -1
- package/config/table-headers.js +2 -2
- package/config/types.js +15 -6
- package/core/plugin.ts +32 -7
- package/core/types.ts +18 -1
- package/detail/{cis.cattle.io.clusterscan.vue → compliance.cattle.io.clusterscan.vue} +22 -18
- package/detail/management.cattle.io.fleetworkspace.vue +18 -27
- package/detail/management.cattle.io.oidcclient.vue +369 -0
- package/detail/node.vue +2 -2
- package/detail/pod.vue +2 -2
- package/detail/service.vue +10 -1
- package/detail/workload/index.vue +8 -2
- package/dialog/ExtensionCatalogUninstallDialog.vue +7 -4
- package/dialog/GenericPrompt.vue +1 -1
- package/dialog/ImportDialog.vue +8 -8
- package/dialog/OidcClientSecretDialog.vue +117 -0
- package/edit/__tests__/cis.cattle.io.clusterscan.test.ts +3 -3
- package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +5 -2
- package/edit/autoscaling.horizontalpodautoscaler/index.vue +4 -1
- package/edit/{cis.cattle.io.clusterscan.vue → compliance.cattle.io.clusterscan.vue} +30 -31
- package/edit/{cis.cattle.io.clusterscanbenchmark.vue → compliance.cattle.io.clusterscanbenchmark.vue} +4 -4
- package/edit/{cis.cattle.io.clusterscanprofile.vue → compliance.cattle.io.clusterscanprofile.vue} +5 -5
- package/edit/configmap.vue +4 -1
- package/edit/constraints.gatekeeper.sh.constraint/index.vue +1 -0
- package/edit/fleet.cattle.io.gitrepo.vue +44 -222
- package/edit/fleet.cattle.io.helmop.vue +44 -269
- package/edit/helm.cattle.io.projecthelmchart.vue +1 -0
- package/edit/k8s.cni.cncf.io.networkattachmentdefinition.vue +1 -0
- package/edit/logging-flow/index.vue +1 -0
- package/edit/logging.banzaicloud.io.output/index.vue +1 -0
- package/edit/management.cattle.io.fleetworkspace.vue +1 -0
- package/edit/management.cattle.io.oidcclient.vue +162 -0
- package/edit/management.cattle.io.project.vue +4 -1
- package/edit/monitoring.coreos.com.alertmanagerconfig/index.vue +1 -1
- package/edit/monitoring.coreos.com.alertmanagerconfig/receiverConfig.vue +5 -0
- package/edit/monitoring.coreos.com.prometheusrule/index.vue +1 -0
- package/edit/monitoring.coreos.com.receiver/auth.vue +30 -30
- package/edit/monitoring.coreos.com.receiver/index.vue +1 -0
- package/edit/monitoring.coreos.com.receiver/types/email.vue +1 -1
- package/edit/monitoring.coreos.com.route.vue +1 -0
- package/edit/namespace.vue +1 -0
- package/edit/networking.istio.io.destinationrule/index.vue +4 -1
- package/edit/networking.k8s.io.ingress/index.vue +4 -1
- package/edit/networking.k8s.io.networkpolicy/PolicyRules.vue +7 -2
- package/edit/networking.k8s.io.networkpolicy/index.vue +6 -2
- package/edit/node.vue +1 -0
- package/edit/persistentvolume/index.vue +4 -1
- package/edit/provisioning.cattle.io.cluster/rke2.vue +418 -382
- package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +27 -27
- package/edit/provisioning.cattle.io.cluster/tabs/MachinePool.vue +5 -0
- package/edit/resources.cattle.io.restore.vue +1 -1
- package/edit/secret/index.vue +1 -0
- package/edit/service.vue +4 -1
- package/edit/serviceaccount.vue +4 -1
- package/edit/storage.k8s.io.storageclass/index.vue +4 -1
- package/edit/workload/index.vue +5 -0
- package/list/{cis.cattle.io.clusterscan.vue → compliance.cattle.io.clusterscan.vue} +2 -2
- package/list/management.cattle.io.oidcclient.vue +108 -0
- package/list/node.vue +2 -0
- package/machine-config/amazonec2.vue +3 -24
- package/machine-config/components/GCEImage.vue +374 -0
- package/machine-config/google.vue +617 -0
- package/mixins/__tests__/brand.spec.ts +170 -0
- package/mixins/brand.js +16 -17
- package/mixins/create-edit-view/index.js +5 -0
- package/mixins/resource-fetch-api-pagination.js +16 -0
- package/mixins/vue-select-overrides.js +1 -0
- package/models/{cis.cattle.io.clusterscan.js → compliance.cattle.io.clusterscan.js} +8 -8
- package/models/{cis.cattle.io.clusterscanbenchmark.js → compliance.cattle.io.clusterscanbenchmark.js} +1 -1
- package/models/{cis.cattle.io.clusterscanprofile.js → compliance.cattle.io.clusterscanprofile.js} +5 -5
- package/models/{cis.cattle.io.clusterscanreport.js → compliance.cattle.io.clusterscanreport.js} +1 -1
- package/models/fleet-application.js +8 -79
- package/models/fleet.cattle.io.cluster.js +11 -0
- package/models/fleet.cattle.io.gitrepo.js +2 -2
- package/models/fleet.cattle.io.helmop.js +9 -39
- package/models/management.cattle.io.fleetworkspace.js +2 -1
- package/models/management.cattle.io.oidcclient.js +18 -0
- package/models/management.cattle.io.registration.js +3 -0
- package/models/provisioning.cattle.io.cluster.js +5 -5
- package/models/service.js +4 -0
- package/models/workload.js +5 -0
- package/package.json +1 -1
- package/pages/about.vue +4 -58
- package/pages/auth/login.vue +1 -1
- package/pages/c/_cluster/apps/charts/AddRepoLink.vue +0 -1
- package/pages/c/_cluster/apps/charts/index.vue +285 -81
- package/pages/c/_cluster/auth/user.retention/index.vue +87 -78
- package/pages/c/_cluster/explorer/index.vue +3 -3
- package/pages/c/_cluster/explorer/tools/pages/_page.vue +0 -1
- package/pages/c/_cluster/fleet/application/create.vue +3 -2
- package/pages/c/_cluster/fleet/index.vue +94 -56
- package/pages/c/_cluster/fleet/settings/index.vue +229 -0
- package/pages/c/_cluster/longhorn/index.vue +5 -2
- package/pages/c/_cluster/uiplugins/CatalogList/index.vue +16 -1
- package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +2 -2
- package/pages/explorer/resource/detail/configmap.vue +30 -7
- package/pages/explorer/resource/detail/secret.vue +50 -0
- package/pages/home.vue +9 -55
- package/pages/support/index.vue +4 -6
- package/plugins/dashboard-store/actions.js +19 -5
- package/plugins/dashboard-store/getters.js +4 -0
- package/plugins/dashboard-store/resource-class.js +16 -2
- package/plugins/steve/steve-pagination-utils.ts +26 -18
- package/plugins/steve/subscribe.js +6 -1
- package/rancher-components/Banner/Banner.vue +13 -0
- package/rancher-components/Form/Checkbox/Checkbox.vue +9 -4
- package/rancher-components/Form/LabeledInput/LabeledInput.vue +1 -1
- package/rancher-components/LabeledTooltip/LabeledTooltip.vue +1 -0
- package/rancher-components/RcItemCard/RcItemCard.vue +8 -3
- package/store/auth.js +2 -0
- package/store/catalog.js +23 -1
- package/store/growl.js +97 -8
- package/store/index.js +6 -0
- package/store/notifications.ts +426 -0
- package/store/prefs.js +0 -1
- package/store/type-map.js +19 -16
- package/store/uiplugins.ts +15 -1
- package/types/fleet.d.ts +24 -0
- package/types/notifications/index.ts +74 -0
- package/types/shell/index.d.ts +46 -32
- package/types/store/dashboard-store.types.ts +16 -0
- package/utils/__tests__/fleet.test.ts +148 -0
- package/utils/__tests__/object.test.ts +54 -1
- package/utils/__tests__/string.test.ts +273 -1
- package/utils/__tests__/time.test.ts +31 -0
- package/utils/auth.js +9 -2
- package/utils/crypto/encryption.ts +103 -0
- package/utils/cspAdaptor.ts +51 -0
- package/utils/fleet.ts +54 -65
- package/utils/object.js +36 -0
- package/utils/pagination-utils.ts +1 -1
- package/utils/release-notes.ts +48 -0
- package/utils/selector-typed.ts +7 -2
- package/utils/string.js +24 -0
- package/utils/{time.js → time.ts} +25 -6
- package/utils/uiplugins.ts +22 -0
- package/utils/validators/formRules/index.ts +3 -0
- package/components/Resource/Detail/TitleBar/composable.ts +0 -31
- package/config/product/legacy.js +0 -62
- package/pages/c/_cluster/legacy/pages/_page.vue +0 -29
- package/pages/c/_cluster/legacy/project/_page.vue +0 -57
- package/pages/c/_cluster/legacy/project/index.vue +0 -32
- package/pages/c/_cluster/legacy/project/pipelines.vue +0 -96
- /package/components/ResourceDetail/{Masthead.vue → Masthead/legacy.vue} +0 -0
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import day from 'dayjs';
|
|
3
|
+
import { DATE_FORMAT, TIME_FORMAT } from '@shell/store/prefs';
|
|
4
|
+
import { escapeHtml } from '@shell/utils/string';
|
|
5
|
+
import { computed, inject, ref } from 'vue';
|
|
6
|
+
import { useStore } from 'vuex';
|
|
7
|
+
import { useRouter } from 'vue-router';
|
|
8
|
+
import { useI18n } from '@shell/composables/useI18n';
|
|
9
|
+
import { NotificationAction, NotificationLevel, StoredNotification } from '@shell/types/notifications';
|
|
10
|
+
import { DropdownContext, defaultContext } from '@components/RcDropdown/types';
|
|
11
|
+
import { useDropdownItem } from '@components/RcDropdown/useDropdownItem';
|
|
12
|
+
|
|
13
|
+
const CLASSES = {
|
|
14
|
+
[NotificationLevel.Announcement]: 'icon-notify-announcement text-info',
|
|
15
|
+
[NotificationLevel.Error]: 'icon-notify-error text-error',
|
|
16
|
+
[NotificationLevel.Info]: 'icon-notify-info text-info',
|
|
17
|
+
[NotificationLevel.Task]: 'icon-notify-busy text-info',
|
|
18
|
+
[NotificationLevel.Warning]: 'icon-notify-warning text-warning',
|
|
19
|
+
[NotificationLevel.Success]: 'icon-notify-tick text-success',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const emits = defineEmits(['didFocus']);
|
|
23
|
+
|
|
24
|
+
const props = defineProps<{item: StoredNotification}>();
|
|
25
|
+
const { dropdownItems } = inject<DropdownContext>('dropdownContext') || defaultContext;
|
|
26
|
+
|
|
27
|
+
const store = useStore();
|
|
28
|
+
const { t } = useI18n(store);
|
|
29
|
+
const router = useRouter();
|
|
30
|
+
const unreadCount = computed<number>(() => store.getters['notifications/unreadCount']);
|
|
31
|
+
const dateFormat = escapeHtml( store.getters['prefs/get'](DATE_FORMAT));
|
|
32
|
+
const timeFormat = escapeHtml( store.getters['prefs/get'](TIME_FORMAT));
|
|
33
|
+
|
|
34
|
+
const { close } = useDropdownItem();
|
|
35
|
+
|
|
36
|
+
// Outer element for the notification
|
|
37
|
+
const dropdownMenuItem = ref<HTMLElement>();
|
|
38
|
+
|
|
39
|
+
// Refs to the 3 buttons that can be in the notification
|
|
40
|
+
const readButton = ref<HTMLElement>();
|
|
41
|
+
const primaryActionButton = ref<HTMLElement>();
|
|
42
|
+
const secondaryActionButton = ref<HTMLElement>();
|
|
43
|
+
|
|
44
|
+
// These are the buttons in the notification that can be tabbed between when in the focus trap
|
|
45
|
+
const tabItems = computed(() => {
|
|
46
|
+
const items: HTMLElement[] = [];
|
|
47
|
+
|
|
48
|
+
if (readButton.value) {
|
|
49
|
+
items.push(readButton.value);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (props.item.primaryAction && primaryActionButton.value) {
|
|
53
|
+
items.push(primaryActionButton.value);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (props.item.secondaryAction && secondaryActionButton.value) {
|
|
57
|
+
items.push(secondaryActionButton.value);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return items;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Ensure the aria label changes when read/unread is toggled
|
|
64
|
+
const toggleLabel = computed(() => {
|
|
65
|
+
return props.item.read ? t('notificationCenter.markRead') : t('notificationCenter.markUnread');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const age = computed(() => {
|
|
69
|
+
const created = day(props.item?.created);
|
|
70
|
+
const diff = created.diff(day(), 'day');
|
|
71
|
+
let date = created.format(dateFormat);
|
|
72
|
+
|
|
73
|
+
if (diff === 0 ) {
|
|
74
|
+
date = t('notificationCenter.dates.today');
|
|
75
|
+
} else if (diff === 1) {
|
|
76
|
+
date = t('notificationCenter.dates.yesterday');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return `${ date }, ${ created.format(timeFormat) }`;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const clz = computed(() => CLASSES[props.item.level]);
|
|
83
|
+
|
|
84
|
+
// Invoke action on either the primary or secondary buttons
|
|
85
|
+
// This can open a URL in a new tab OR navigate to an application route
|
|
86
|
+
const action = (action: NotificationAction) => {
|
|
87
|
+
if (action.target) {
|
|
88
|
+
window.open(action.target, '_blank');
|
|
89
|
+
} else if (action.route) {
|
|
90
|
+
try {
|
|
91
|
+
router.push(action.route);
|
|
92
|
+
} catch (e) {
|
|
93
|
+
console.error('Error navigating to route for the notification action', e); // eslint-disable-line no-console
|
|
94
|
+
}
|
|
95
|
+
close();
|
|
96
|
+
} else {
|
|
97
|
+
console.error('Notification action must either specify a "target" or a "route"'); // eslint-disable-line no-console
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const toggleRead = (e: MouseEvent | KeyboardEvent, fromKeyboard = false) => {
|
|
102
|
+
if (props.item.read) {
|
|
103
|
+
store.dispatch('notifications/markUnread', props.item.id);
|
|
104
|
+
} else {
|
|
105
|
+
store.dispatch('notifications/markRead', props.item.id);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (fromKeyboard) {
|
|
109
|
+
e.preventDefault();
|
|
110
|
+
e.stopPropagation();
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// User presses enter or space on the notification, so enter the focus trap
|
|
115
|
+
const enterFocusTrap = (e: MouseEvent | KeyboardEvent) => {
|
|
116
|
+
const elementToFocus = tabItems.value[0];
|
|
117
|
+
|
|
118
|
+
elementToFocus.focus();
|
|
119
|
+
|
|
120
|
+
e.preventDefault();
|
|
121
|
+
e.stopPropagation();
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// Inner focus next
|
|
125
|
+
// Move the tab between the buttons within the notification when it is in a focus trap
|
|
126
|
+
const innerFocusNext = (e: KeyboardEvent) => {
|
|
127
|
+
const index = tabItems.value.indexOf(e.target as HTMLElement);
|
|
128
|
+
|
|
129
|
+
// Sanity check - shouldn't happen
|
|
130
|
+
if (index < 0) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let nextIndex = e.shiftKey ? index - 1 : index + 1;
|
|
135
|
+
|
|
136
|
+
if (nextIndex < 0) {
|
|
137
|
+
nextIndex = tabItems.value.length - 1;
|
|
138
|
+
} else if (nextIndex === tabItems.value.length) {
|
|
139
|
+
nextIndex = 0;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const nextElement = tabItems.value[nextIndex] as HTMLElement;
|
|
143
|
+
|
|
144
|
+
nextElement.focus();
|
|
145
|
+
|
|
146
|
+
e.stopPropagation();
|
|
147
|
+
e.preventDefault();
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// Exit inner focus - this is the state when the notification has a focus trap and you are tabbing between
|
|
151
|
+
// the controls in the notification - this function is typically called when the user presses ESCape
|
|
152
|
+
// and we want to exit this focus trap
|
|
153
|
+
const exitFocusTrap = (e: MouseEvent | KeyboardEvent) => {
|
|
154
|
+
// Return focus to the outer notification div
|
|
155
|
+
dropdownMenuItem.value?.focus();
|
|
156
|
+
|
|
157
|
+
e.stopPropagation();
|
|
158
|
+
e.preventDefault();
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const gotFocus = () => {
|
|
162
|
+
const activeItem = document.activeElement;
|
|
163
|
+
const activeIndex = dropdownItems.value.indexOf(activeItem || new HTMLElement());
|
|
164
|
+
|
|
165
|
+
// Let the scroll container decide if it needs to adjust the scroll to show the item fully
|
|
166
|
+
emits('didFocus', activeIndex, dropdownItems.value.length);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const handleKeydown = (e: KeyboardEvent) => {
|
|
170
|
+
const activeItem = document.activeElement;
|
|
171
|
+
const activeIndex = dropdownItems.value.indexOf(activeItem || new HTMLElement());
|
|
172
|
+
|
|
173
|
+
if (activeIndex < 0) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const shouldAdvance = e.key === 'ArrowDown';
|
|
178
|
+
const newIndex = findNewIndex(shouldAdvance, activeIndex, dropdownItems.value);
|
|
179
|
+
|
|
180
|
+
if (dropdownItems.value[newIndex] instanceof HTMLElement) {
|
|
181
|
+
dropdownItems.value[newIndex].focus();
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* This allows the user to press up/down while in the focus trap for a notification and exit the focus trap and move to the next/previous notification
|
|
187
|
+
*/
|
|
188
|
+
const handleKeydownFocusTrap = (e: KeyboardEvent) => {
|
|
189
|
+
exitFocusTrap(e);
|
|
190
|
+
handleKeydown(e);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Finds the new index for the dropdown item based on the key pressed.
|
|
195
|
+
* @param shouldAdvance - Whether to advance to the next or previous item.
|
|
196
|
+
* @param activeIndex - Current active index.
|
|
197
|
+
* @param itemsArr - Array of dropdown items.
|
|
198
|
+
* @returns The new index.
|
|
199
|
+
*/
|
|
200
|
+
const findNewIndex = (shouldAdvance: boolean, activeIndex: number, itemsArr: Element[]) => {
|
|
201
|
+
const newIndex = shouldAdvance ? activeIndex + 1 : activeIndex - 1;
|
|
202
|
+
|
|
203
|
+
if (!shouldAdvance && activeIndex === 1 && unreadCount.value === 0) {
|
|
204
|
+
// Special case
|
|
205
|
+
// We are the top notification, there are no unread and the uses has pressed up
|
|
206
|
+
// We want to skip the notification header, as this won't have the 'Mark all as read' button
|
|
207
|
+
return itemsArr.length - 1;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (newIndex > itemsArr.length - 1) {
|
|
211
|
+
return 0;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (newIndex < 0) {
|
|
215
|
+
return itemsArr.length - 1;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return newIndex;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Ensure we scroll the item into view smoothly
|
|
223
|
+
* @param event FocusIn Event
|
|
224
|
+
*/
|
|
225
|
+
const scrollIntoView = (event: Event) => {
|
|
226
|
+
const target = event.target;
|
|
227
|
+
|
|
228
|
+
if (target instanceof HTMLElement) {
|
|
229
|
+
const t = target as HTMLElement;
|
|
230
|
+
|
|
231
|
+
// If a button was clicked, then do not scroll into view, as this will scroll to make the button
|
|
232
|
+
// visible and the click will be ignored - so just return, so that the click works as expected
|
|
233
|
+
if (t.tagName === 'BUTTON') {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (target instanceof HTMLElement) {
|
|
239
|
+
target?.scrollIntoView({
|
|
240
|
+
behavior: 'smooth',
|
|
241
|
+
block: 'center',
|
|
242
|
+
inline: 'nearest',
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
</script>
|
|
247
|
+
|
|
248
|
+
<template>
|
|
249
|
+
<div
|
|
250
|
+
ref="dropdownMenuItem"
|
|
251
|
+
dropdown-menu-item
|
|
252
|
+
tabindex="-1"
|
|
253
|
+
role="menuitem"
|
|
254
|
+
data-testid="notifications-center-item"
|
|
255
|
+
:aria-label="t('notificationCenter.ariaLabel', { title: item.title })"
|
|
256
|
+
:class="{ 'notification-unread': !item.read }"
|
|
257
|
+
@keydown.up.down.stop.prevent="handleKeydown"
|
|
258
|
+
@focusin="scrollIntoView"
|
|
259
|
+
@focus.stop="gotFocus"
|
|
260
|
+
@click.stop
|
|
261
|
+
@keydown.enter.space.stop="enterFocusTrap"
|
|
262
|
+
>
|
|
263
|
+
<div
|
|
264
|
+
class="notification"
|
|
265
|
+
:data-testid="`notifications-center-item-${ item.id }`"
|
|
266
|
+
>
|
|
267
|
+
<div class="sep" />
|
|
268
|
+
<div class="top">
|
|
269
|
+
<div class="icon">
|
|
270
|
+
<i
|
|
271
|
+
class="icon"
|
|
272
|
+
:class="clz"
|
|
273
|
+
/>
|
|
274
|
+
</div>
|
|
275
|
+
<div class="item-title">
|
|
276
|
+
{{ item.title }}
|
|
277
|
+
</div>
|
|
278
|
+
<button
|
|
279
|
+
ref="readButton"
|
|
280
|
+
v-clean-tooltip="item.read ? t('notificationCenter.markUnread') : t('notificationCenter.markRead')"
|
|
281
|
+
class="read-indicator"
|
|
282
|
+
role="button"
|
|
283
|
+
:aria-label="toggleLabel"
|
|
284
|
+
@keydown.enter.space.stop="toggleRead($event, true)"
|
|
285
|
+
@keydown.tab.stop="innerFocusNext($event)"
|
|
286
|
+
@keydown.escape.stop="exitFocusTrap"
|
|
287
|
+
@keydown.up.down.prevent.stop="handleKeydownFocusTrap"
|
|
288
|
+
@click.stop="toggleRead($event, false)"
|
|
289
|
+
>
|
|
290
|
+
<div>
|
|
291
|
+
<div
|
|
292
|
+
class="read-icon"
|
|
293
|
+
:class="{ 'unread': !item.read }"
|
|
294
|
+
/>
|
|
295
|
+
</div>
|
|
296
|
+
</button>
|
|
297
|
+
</div>
|
|
298
|
+
<div class="bottom">
|
|
299
|
+
<div class="created text-muted">
|
|
300
|
+
{{ age }}
|
|
301
|
+
</div>
|
|
302
|
+
<div
|
|
303
|
+
v-if="item.message"
|
|
304
|
+
class="message"
|
|
305
|
+
>
|
|
306
|
+
{{ item.message }}
|
|
307
|
+
</div>
|
|
308
|
+
<div
|
|
309
|
+
v-if="item.level === NotificationLevel.Task && typeof item.progress === 'number'"
|
|
310
|
+
class="progress"
|
|
311
|
+
>
|
|
312
|
+
<div class="progress-bar">
|
|
313
|
+
<div class="pb-background" />
|
|
314
|
+
<div
|
|
315
|
+
:style="{width: `${item.progress}%`}"
|
|
316
|
+
class="pb-foreground"
|
|
317
|
+
/>
|
|
318
|
+
</div>
|
|
319
|
+
<div class="progress-percent text-muted">
|
|
320
|
+
{{ item.progress }}%
|
|
321
|
+
</div>
|
|
322
|
+
</div>
|
|
323
|
+
<div
|
|
324
|
+
v-if="item.primaryAction || item.secondaryAction"
|
|
325
|
+
class="notification-actions"
|
|
326
|
+
>
|
|
327
|
+
<button
|
|
328
|
+
v-if="item.primaryAction"
|
|
329
|
+
ref="primaryActionButton"
|
|
330
|
+
role="button"
|
|
331
|
+
class="btn btn-sm role-primary"
|
|
332
|
+
@keydown.enter.space.stop="action(item.primaryAction)"
|
|
333
|
+
@keydown.tab.stop="innerFocusNext($event)"
|
|
334
|
+
@keydown.escape.stop="exitFocusTrap"
|
|
335
|
+
@click.stop.prevent="action(item.primaryAction)"
|
|
336
|
+
@keydown.up.down.prevent.stop="handleKeydownFocusTrap"
|
|
337
|
+
>
|
|
338
|
+
{{ item.primaryAction.label }}
|
|
339
|
+
</button>
|
|
340
|
+
<button
|
|
341
|
+
v-if="item.secondaryAction"
|
|
342
|
+
ref="secondaryActionButton"
|
|
343
|
+
role="button"
|
|
344
|
+
class="btn btn-sm role-secondary"
|
|
345
|
+
@keydown.enter.space.stop="action(item.secondaryAction)"
|
|
346
|
+
@keydown.tab.stop="innerFocusNext($event)"
|
|
347
|
+
@keydown.escape.stop="exitFocusTrap"
|
|
348
|
+
@click.stop.prevent="action(item.secondaryAction)"
|
|
349
|
+
@keydown.up.down.prevent.stop="handleKeydownFocusTrap"
|
|
350
|
+
>
|
|
351
|
+
{{ item.secondaryAction.label }}
|
|
352
|
+
</button>
|
|
353
|
+
</div>
|
|
354
|
+
</div>
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
</template>
|
|
358
|
+
|
|
359
|
+
<style lang="scss" scoped>
|
|
360
|
+
[dropdown-menu-item] {
|
|
361
|
+
display: flex;
|
|
362
|
+
gap: 8px;
|
|
363
|
+
align-items: center;
|
|
364
|
+
padding: 12px;
|
|
365
|
+
margin: 0 3px;
|
|
366
|
+
|
|
367
|
+
&.notification-unread {
|
|
368
|
+
background-color: var(--notification-unread-bg);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
&:focus-visible, &:focus {
|
|
372
|
+
@include focus-outline;
|
|
373
|
+
outline-offset: 0;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.notification {
|
|
377
|
+
width: 400px;
|
|
378
|
+
display: flex;
|
|
379
|
+
flex-direction: column;
|
|
380
|
+
|
|
381
|
+
.top {
|
|
382
|
+
align-items: center;
|
|
383
|
+
display: flex;
|
|
384
|
+
padding: 0;
|
|
385
|
+
|
|
386
|
+
.icon {
|
|
387
|
+
display: flex;
|
|
388
|
+
text-align: center;
|
|
389
|
+
vertical-align: middle;
|
|
390
|
+
width: 32px;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.item-title {
|
|
394
|
+
flex: 1;
|
|
395
|
+
font-weight: 700;
|
|
396
|
+
white-space: nowrap;
|
|
397
|
+
overflow: hidden;
|
|
398
|
+
text-overflow: ellipsis;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
button.read-indicator {
|
|
402
|
+
line-height: normal;
|
|
403
|
+
min-height: auto;
|
|
404
|
+
padding: 8px;
|
|
405
|
+
margin-left: 16px;
|
|
406
|
+
background-color: unset;
|
|
407
|
+
display: flex;
|
|
408
|
+
align-items: center;
|
|
409
|
+
justify-content: center;
|
|
410
|
+
|
|
411
|
+
&:disabled {
|
|
412
|
+
cursor: default;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
&:focus-visible {
|
|
416
|
+
@include focus-outline;
|
|
417
|
+
outline-offset: 2px;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
.read-icon {
|
|
421
|
+
border: 2px solid var(--primary);
|
|
422
|
+
border-radius: 50%;
|
|
423
|
+
width: 8px;
|
|
424
|
+
height: 8px;
|
|
425
|
+
|
|
426
|
+
&.unread {
|
|
427
|
+
background-color: var(--primary);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Add subtle effect when hovering over the unread button
|
|
432
|
+
&:hover .read-icon.unread {
|
|
433
|
+
opacity: 0.5;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
.bottom {
|
|
439
|
+
margin-left: 32px; // 20px icon + 12px spacing
|
|
440
|
+
|
|
441
|
+
.created {
|
|
442
|
+
font-size: 13px;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
.message {
|
|
446
|
+
line-height: 20px;
|
|
447
|
+
padding: 6px 0;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
.progress {
|
|
451
|
+
display: flex;
|
|
452
|
+
margin-top: 6px;
|
|
453
|
+
|
|
454
|
+
.progress-bar {
|
|
455
|
+
align-items: center;
|
|
456
|
+
display: flex;
|
|
457
|
+
position: relative;
|
|
458
|
+
flex: 1;
|
|
459
|
+
|
|
460
|
+
.pb-foreground, .pb-background {
|
|
461
|
+
position: absolute;
|
|
462
|
+
height: 6px;
|
|
463
|
+
border-radius: 5px;
|
|
464
|
+
background-color: var(--primary);
|
|
465
|
+
transition: width 0.1s ease-in;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
.pb-background {
|
|
469
|
+
opacity: 0.5;
|
|
470
|
+
width: 100%;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
.progress-percent {
|
|
475
|
+
font-size: 13px;
|
|
476
|
+
margin-left: 16px;
|
|
477
|
+
min-width: 40px;
|
|
478
|
+
text-align: right;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
.notification-actions {
|
|
483
|
+
display: flex;
|
|
484
|
+
margin-top: 12px;
|
|
485
|
+
|
|
486
|
+
> button:not(:first-child) {
|
|
487
|
+
margin-left: 12px;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
.icon {
|
|
493
|
+
font-size: 20px;
|
|
494
|
+
width: 32px;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
.content {
|
|
498
|
+
flex: 1;
|
|
499
|
+
display: flex;
|
|
500
|
+
flex-direction: column;
|
|
501
|
+
align-content: flex-start;
|
|
502
|
+
|
|
503
|
+
.item-title {
|
|
504
|
+
font-weight: bold;
|
|
505
|
+
margin-bottom: 4px;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
</style>
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useStore } from 'vuex';
|
|
3
|
+
import { computed, inject, ref } from 'vue';
|
|
4
|
+
import { DropdownContext, defaultContext } from '@components/RcDropdown/types';
|
|
5
|
+
|
|
6
|
+
const { dropdownItems } = inject<DropdownContext>('dropdownContext') || defaultContext;
|
|
7
|
+
const store = useStore();
|
|
8
|
+
const unreadCount = computed<number>(() => store.getters['notifications/unreadCount']);
|
|
9
|
+
const markAllReadButton = ref<HTMLElement>();
|
|
10
|
+
|
|
11
|
+
const markAllRead = (keyboard: boolean) => {
|
|
12
|
+
store.dispatch('notifications/markAllRead');
|
|
13
|
+
|
|
14
|
+
// If we have focus, then move to the next item if activated by the keyboard
|
|
15
|
+
if (keyboard && document.activeElement === markAllReadButton?.value) {
|
|
16
|
+
moveFocus(true);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Handles keydown events to navigate between dropdown items.
|
|
22
|
+
* @param {KeyboardEvent} e - The keydown event.
|
|
23
|
+
*/
|
|
24
|
+
const handleKeydown = (e: KeyboardEvent) => {
|
|
25
|
+
const shouldAdvance = e.key === 'ArrowDown';
|
|
26
|
+
|
|
27
|
+
moveFocus(shouldAdvance);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// User pressed up/down - move focus to the correct notification item
|
|
31
|
+
// Header should always be the first item
|
|
32
|
+
const moveFocus = (shouldAdvance: Boolean) => {
|
|
33
|
+
if (dropdownItems.value.length > 1) {
|
|
34
|
+
const newIndex = shouldAdvance ? 1 : dropdownItems.value.length - 1;
|
|
35
|
+
|
|
36
|
+
if (dropdownItems.value[newIndex] instanceof HTMLElement) {
|
|
37
|
+
dropdownItems.value[newIndex].focus();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const gotFocus = (e: Event) => {
|
|
43
|
+
// If no unread items, then there is no button to focus, so move to the next item
|
|
44
|
+
if (unreadCount.value === 0) {
|
|
45
|
+
moveFocus(true);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// When the header gets focus, pass the focus onto the 'mark all read' button
|
|
49
|
+
markAllReadButton?.value?.focus();
|
|
50
|
+
};
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<template>
|
|
54
|
+
<div
|
|
55
|
+
dropdown-menu-item
|
|
56
|
+
tabindex="-1"
|
|
57
|
+
role="menuitem"
|
|
58
|
+
class="notification-header"
|
|
59
|
+
@click.stop
|
|
60
|
+
@keydown.up.down.stop.prevent="handleKeydown"
|
|
61
|
+
@focus="gotFocus"
|
|
62
|
+
>
|
|
63
|
+
<div class="notification-header">
|
|
64
|
+
<div class="notification-title">
|
|
65
|
+
{{ t('notificationCenter.title') }}
|
|
66
|
+
</div>
|
|
67
|
+
<div v-if="unreadCount !== 0">
|
|
68
|
+
<a
|
|
69
|
+
ref="markAllReadButton"
|
|
70
|
+
role="button"
|
|
71
|
+
tabindex="-1"
|
|
72
|
+
href="#"
|
|
73
|
+
data-testid="notifications-center-markall-read"
|
|
74
|
+
@keydown.up.down.stop.prevent="handleKeydown"
|
|
75
|
+
@keydown.enter.space.stop="markAllRead(true)"
|
|
76
|
+
@click="markAllRead(false)"
|
|
77
|
+
>
|
|
78
|
+
{{ t('notificationCenter.markAllRead') }}
|
|
79
|
+
</a>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
<div class="notification-border" />
|
|
83
|
+
</div>
|
|
84
|
+
</template>
|
|
85
|
+
|
|
86
|
+
<style lang="scss" scoped>
|
|
87
|
+
[dropdown-menu-item] {
|
|
88
|
+
display: flex;
|
|
89
|
+
flex-direction: column;
|
|
90
|
+
|
|
91
|
+
.notification-border {
|
|
92
|
+
border-bottom: 1px solid var(--border);
|
|
93
|
+
margin-left: -16px;
|
|
94
|
+
width: calc(100% + 32px);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.notification-header {
|
|
98
|
+
display: flex;
|
|
99
|
+
flex: 1;
|
|
100
|
+
padding: 6px 16px 16px 16px; // already 10px at the top of the dropdown
|
|
101
|
+
|
|
102
|
+
.notification-title {
|
|
103
|
+
font-weight: bold;
|
|
104
|
+
flex: 1;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
A {
|
|
108
|
+
color: var(--link);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
</style>
|