From 0ee98916ba34e25a381446b5e93008fd17fcf02d Mon Sep 17 00:00:00 2001 From: Alexander Onnikov Date: Thu, 30 Oct 2025 23:00:52 +0700 Subject: [PATCH 1/2] fix: optimize editor toolbar loading state check (#10174) Signed-off-by: Alexander Onnikov --- .../src/components/extension/embed/embed.ts | 18 ++-- .../extension/embed/providers/drive.ts | 5 +- .../extension/embed/providers/youtube.ts | 2 +- .../src/components/extension/imageExt.ts | 8 +- .../components/extension/toolbar/toolbar.ts | 100 ++++++++++++++---- 5 files changed, 103 insertions(+), 30 deletions(-) diff --git a/plugins/text-editor-resources/src/components/extension/embed/embed.ts b/plugins/text-editor-resources/src/components/extension/embed/embed.ts index 4205149b80..9ce13bb8ca 100644 --- a/plugins/text-editor-resources/src/components/extension/embed/embed.ts +++ b/plugins/text-editor-resources/src/components/extension/embed/embed.ts @@ -48,7 +48,11 @@ export interface EmbedNodeProvider { autoEmbedUrl?: (src: string) => boolean } -export type EmbedNodeView = (editor: Editor, root: HTMLDivElement) => EmbedNodeViewHandle | undefined +export type EmbedNodeView = ( + editor: Editor, + root: HTMLDivElement, + getPos: () => number +) => EmbedNodeViewHandle | undefined export type EmbedNodeProviderConstructor = (options: T) => EmbedNodeProvider export type EmbedCursor = ToolbarCursor @@ -101,7 +105,7 @@ export const EmbedNode = BaseEmbedNode.extend({ }, addNodeView () { - return ({ node, HTMLAttributes, editor }) => { + return ({ node, HTMLAttributes, editor, getPos }) => { const providerPromise = matchUrl(this.options.providers, node.attrs.src) const root = document.createElement('div') @@ -109,7 +113,8 @@ export const EmbedNode = BaseEmbedNode.extend({ root.setAttribute('data-embed-src', node.attrs.src) root.classList.add('embed-node') - setLoadingState(editor.view, root, true) + const pos = typeof getPos === 'function' ? getPos() : 0 + setLoadingState(editor.view, pos, true) root.setAttribute('block-editor-blur', 'true') let handle: EmbedNodeViewHandle | undefined @@ -117,13 +122,14 @@ export const EmbedNode = BaseEmbedNode.extend({ void providerPromise .then((view) => { view = view ?? StubEmbedNodeView - handle = view(editor, root) + handle = view(editor, root, getPos) if (handle !== undefined) { root.classList.add(`embed-${handle.name}`) } }) .finally(() => { - setLoadingState(editor.view, root, false) + const pos = typeof getPos === 'function' ? getPos() : 0 + setLoadingState(editor.view, pos, false) }) return { @@ -500,7 +506,7 @@ export function replacePreviewContent ( return tr } -const StubEmbedNodeView: EmbedNodeView = (editor: Editor, root: HTMLElement) => { +const StubEmbedNodeView: EmbedNodeView = (editor: Editor, root: HTMLDivElement, getPos: () => number) => { const hint = document.createElement('p') const hintIcon = hint.appendChild(document.createElementNS('http://www.w3.org/2000/svg', 'svg')) const hintSpan = hint.appendChild(document.createElement('span')) diff --git a/plugins/text-editor-resources/src/components/extension/embed/providers/drive.ts b/plugins/text-editor-resources/src/components/extension/embed/providers/drive.ts index c7c1e750a6..a850be0666 100644 --- a/plugins/text-editor-resources/src/components/extension/embed/providers/drive.ts +++ b/plugins/text-editor-resources/src/components/extension/embed/providers/drive.ts @@ -59,9 +59,10 @@ export const DriveEmbedProvider: EmbedNodeProviderConstructor if (previewType === undefined) return - return (editor: Editor, root: HTMLDivElement) => { + return (editor: Editor, root: HTMLDivElement, getPos: () => number) => { const setLoading = (loading: boolean): void => { - setLoadingState(editor.view, root, loading) + const pos = typeof getPos === 'function' ? getPos() : 0 + setLoadingState(editor.view, pos, loading) } const renderer = new SvelteRenderer(FilePreview as any, { element: root, diff --git a/plugins/text-editor-resources/src/components/extension/embed/providers/youtube.ts b/plugins/text-editor-resources/src/components/extension/embed/providers/youtube.ts index 7fb0da57d9..0004c9e685 100644 --- a/plugins/text-editor-resources/src/components/extension/embed/providers/youtube.ts +++ b/plugins/text-editor-resources/src/components/extension/embed/providers/youtube.ts @@ -21,7 +21,7 @@ export const YoutubeEmbedProvider: EmbedNodeProviderConstructor { + return (editor: Editor, root: HTMLDivElement, getPos: () => number) => { root.setAttribute('data-block-toolbar-mouse-lock', 'true') const iframe = document.createElement('iframe') diff --git a/plugins/text-editor-resources/src/components/extension/imageExt.ts b/plugins/text-editor-resources/src/components/extension/imageExt.ts index 124fff20a4..617db0de12 100644 --- a/plugins/text-editor-resources/src/components/extension/imageExt.ts +++ b/plugins/text-editor-resources/src/components/extension/imageExt.ts @@ -151,7 +151,7 @@ export const ImageExtension = ImageNode.extend({ addNodeView () { const imageSrcCache = new Map() - return ({ view, node, HTMLAttributes }) => { + return ({ view, node, HTMLAttributes, getPos }) => { const container = document.createElement('div') const imgElement = document.createElement('img') container.append(imgElement) @@ -161,12 +161,14 @@ export const ImageExtension = ImageNode.extend({ 'data-align': node.attrs.align } - setLoadingState(view, container, true) + const pos = typeof getPos === 'function' ? getPos() : 0 + setLoadingState(view, pos, true) const setImageProps = (src: string | null, srcset: string | null): void => { if (src != null) imgElement.src = src if (srcset != null) imgElement.srcset = srcset void imgElement.decode().finally(() => { - setLoadingState(view, container, false) + const pos = typeof getPos === 'function' ? getPos() : 0 + setLoadingState(view, pos, false) }) } diff --git a/plugins/text-editor-resources/src/components/extension/toolbar/toolbar.ts b/plugins/text-editor-resources/src/components/extension/toolbar/toolbar.ts index fa1dd06fa5..9405e844f8 100644 --- a/plugins/text-editor-resources/src/components/extension/toolbar/toolbar.ts +++ b/plugins/text-editor-resources/src/components/extension/toolbar/toolbar.ts @@ -88,7 +88,7 @@ export interface SelectionCursorContext { export const ToolbarExtension = Extension.create({ addProseMirrorPlugins () { - return [ToolbarControlPlugin(this.editor, this.options)] + return [LoadingStatePlugin(), ToolbarControlPlugin(this.editor, this.options)] } }) @@ -98,6 +98,67 @@ export interface ToolbarControlTxMeta { providers?: Array> } +export const loadingStatePluginKey = new PluginKey('loadingState') + +export interface LoadingStatePluginState { + loadingNodes: Set +} + +export interface LoadingStateTxMeta { + setLoading?: { pos: number, loading: boolean } +} + +export function LoadingStatePlugin (): Plugin { + return new Plugin({ + key: loadingStatePluginKey, + + state: { + init: () => ({ + loadingNodes: new Set() + }), + + apply (tr, prevState) { + const meta = tr.getMeta(loadingStatePluginKey) as LoadingStateTxMeta | undefined + let loadingNodes = prevState.loadingNodes + + // Handle loading state changes + if (meta?.setLoading !== undefined) { + loadingNodes = new Set(loadingNodes) + if (meta.setLoading.loading) { + loadingNodes.add(meta.setLoading.pos) + } else { + loadingNodes.delete(meta.setLoading.pos) + } + } + + // Map positions through document changes + if (tr.docChanged && loadingNodes.size > 0) { + const mapped = new Set() + loadingNodes.forEach((pos) => { + try { + const mappedPos = tr.mapping.map(pos, -1) + // Verify position is still valid + if (mappedPos >= 0 && mappedPos < tr.doc.content.size) { + mapped.add(mappedPos) + } + } catch (e) { + // Position no longer valid, skip it + console.debug('LoadingStatePlugin: position no longer valid', pos) + } + }) + loadingNodes = mapped + } + + return { loadingNodes } + } + } + }) +} + +export function getLoadingState (state: EditorState): LoadingStatePluginState | undefined { + return loadingStatePluginKey.getState(state) as LoadingStatePluginState | undefined +} + export const toolbarPluginKey = new PluginKey('dynamicToolbar') export interface ToolbarControlPluginState { @@ -326,6 +387,7 @@ export function ToolbarControlPlugin (editor: Editor, options: ToolbarOptions): editor.on('transaction', ({ editor, transaction: tr }) => { const meta = tr.getMeta(toolbarPluginKey) as ToolbarControlTxMeta const loadingState = tr.getMeta('loadingState') as boolean | undefined + if (meta?.cursor !== undefined && !eqCursors(currCursor, meta.cursor)) { prevCursor = currCursor currCursor = meta.cursor !== null ? { ...meta.cursor } : null @@ -506,17 +568,22 @@ function updateCursorFromMouseEvent (view: EditorView, event: MouseEvent): void } function scanForLoadingState (view: EditorView, range: Range): boolean { - let isLoading = false - view.state.doc.nodesBetween(range.from, range.to, (node, pos) => { - const element = view.nodeDOM(pos) - if (!(element instanceof HTMLElement)) return + if (range.from > range.to || range.from < 0) { + return false + } - if (element.dataset.loading === 'true') { - isLoading = true - return false + const loadingState = getLoadingState(view.state) + if (loadingState === undefined || loadingState.loadingNodes.size === 0) { + return false + } + + for (const pos of loadingState.loadingNodes) { + if (pos >= range.from && pos <= range.to) { + return true } - }) - return isLoading + } + + return false } function scanForAnchor (view: EditorView, range: Range): HTMLElement | undefined { @@ -653,15 +720,12 @@ function minmax (value = 0, min = 0, max = 0): number { return Math.min(Math.max(value, min), max) } -export function setLoadingState (view: EditorView, element: HTMLElement, loading: boolean): void { - if (loading) { - element.setAttribute('data-loading', 'true') - } else { - element.removeAttribute('data-loading') - requestAnimationFrame(() => { - view.dispatch(view.state.tr.setMeta('loadingState', loading)) - }) +export function setLoadingState (view: EditorView, pos: number, loading: boolean): void { + const meta: LoadingStateTxMeta = { + setLoading: { pos, loading } } + + view.dispatch(view.state.tr.setMeta(loadingStatePluginKey, meta).setMeta('loadingState', loading)) } export const GeneralToolbarProvider: ToolbarProvider = { From 6a5a7ea6505100e17bd9e0a658d3f6cd7c664629 Mon Sep 17 00:00:00 2001 From: Artyom Savchenko Date: Fri, 31 Oct 2025 11:01:14 +0700 Subject: [PATCH 2/2] Display upgrade plan button if limit is reached (#10175) --- common/config/rush/pnpm-lock.yaml | 5 +- models/billing/package.json | 4 +- models/billing/src/index.ts | 7 + packages/theme/styles/_colors.scss | 18 +++ packages/theme/styles/button.scss | 127 ++++++++++++++++++ packages/ui/src/components/Button.svelte | 2 +- .../ui/src/components/ModernDialog.svelte | 4 + packages/ui/src/types.ts | 1 + plugins/billing-assets/lang/cs.json | 5 +- plugins/billing-assets/lang/de.json | 5 +- plugins/billing-assets/lang/en.json | 5 +- plugins/billing-assets/lang/es.json | 5 +- plugins/billing-assets/lang/fr.json | 5 +- plugins/billing-assets/lang/it.json | 5 +- plugins/billing-assets/lang/ja.json | 5 +- plugins/billing-assets/lang/pt.json | 5 +- plugins/billing-assets/lang/ru.json | 5 +- plugins/billing-assets/lang/tr.json | 5 +- plugins/billing-assets/lang/zh.json | 5 +- .../src/components/SubscriptionsModal.svelte | 23 ++++ .../src/components/UpgradeButton.svelte | 75 +++++++++++ .../src/components/WorkbenchExtension.svelte | 32 +++++ plugins/billing-resources/src/index.ts | 6 +- plugins/billing-resources/src/utils.ts | 109 ++++++++++++++- plugins/billing/src/plugin.ts | 9 +- services/billing/pod-billing/package.json | 3 +- 26 files changed, 459 insertions(+), 21 deletions(-) create mode 100644 plugins/billing-resources/src/components/SubscriptionsModal.svelte create mode 100644 plugins/billing-resources/src/components/UpgradeButton.svelte create mode 100644 plugins/billing-resources/src/components/WorkbenchExtension.svelte diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 68c8f8e4f7..f73f28108c 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -5199,7 +5199,7 @@ packages: version: 0.0.0 '@rush-temp/model-billing@file:projects/model-billing.tgz': - resolution: {integrity: sha512-1eQLuznSm1zZvdCWlGy3hA8xIwRz8RheuLbgmTh/amnobGuia/8ycCJEiPykrsvoLJSiVHrKyaXyySM5+r5jPg==, tarball: file:projects/model-billing.tgz} + resolution: {integrity: sha512-gXVvJywcc4AxaQFQNIy3i/YTJpxtJ7Bo+RSWx4Kn8lVMVSHQ1hK8Crr81hKwP0gNslAXV+TOH+xWRn5WY/xntA==, tarball: file:projects/model-billing.tgz} version: 0.0.0 '@rush-temp/model-bitrix@file:projects/model-bitrix.tgz': @@ -5599,7 +5599,7 @@ packages: version: 0.0.0 '@rush-temp/pod-billing@file:projects/pod-billing.tgz': - resolution: {integrity: sha512-8NzYAsIUJIXwLfBF2mhvK1N70CrgXR/zN4Ppn0YiKrCAPWUaEg6mxz47qvAKyoYZcNvnzGr/6jQbR/Iv7kPEvA==, tarball: file:projects/pod-billing.tgz} + resolution: {integrity: sha512-JaJcuMheg+Um4mTADRrB3cYwpb/XFg9rZQCuh1CAWdZDsKh6g8EoeImFZtD1TeiKXB2wkT+mdbFN0dXYR65LXg==, tarball: file:projects/pod-billing.tgz} version: 0.0.0 '@rush-temp/pod-calendar-mailer@file:projects/pod-calendar-mailer.tgz': @@ -28046,6 +28046,7 @@ snapshots: '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.9.3) cors: 2.8.5 + cross-env: 7.0.3 dotenv: 16.0.3 esbuild: 0.25.11 eslint: 8.57.1 diff --git a/models/billing/package.json b/models/billing/package.json index 678aef35d3..072c057f50 100644 --- a/models/billing/package.json +++ b/models/billing/package.json @@ -37,8 +37,10 @@ "@hcengineering/core": "^0.7.20", "@hcengineering/model": "^0.7.17", "@hcengineering/model-core": "^0.7.0", + "@hcengineering/model-presentation": "^0.7.0", "@hcengineering/setting": "^0.7.0", "@hcengineering/billing": "^0.7.0", - "@hcengineering/platform": "^0.7.17" + "@hcengineering/platform": "^0.7.17", + "@hcengineering/workbench": "^0.7.0" } } diff --git a/models/billing/src/index.ts b/models/billing/src/index.ts index 6fb1a36c6a..b94a3e65d6 100644 --- a/models/billing/src/index.ts +++ b/models/billing/src/index.ts @@ -19,6 +19,8 @@ import { type IntlString } from '@hcengineering/platform' import setting from '@hcengineering/setting' import billing, { type Tier } from '@hcengineering/billing' import { AccountRole, DOMAIN_MODEL } from '@hcengineering/core' +import presentation from '@hcengineering/model-presentation' +import workbench from '@hcengineering/workbench' export { billingId } from '@hcengineering/billing' export { billing as default } @@ -107,4 +109,9 @@ export function createModel (builder: Builder): void { }, billing.tier.Legendary ) + + builder.createDoc(presentation.class.ComponentPointExtension, core.space.Model, { + extension: workbench.extensions.WorkbenchExtensions, + component: billing.component.WorkbenchExtension + }) } diff --git a/packages/theme/styles/_colors.scss b/packages/theme/styles/_colors.scss index de226a53a8..7ef6e0410c 100644 --- a/packages/theme/styles/_colors.scss +++ b/packages/theme/styles/_colors.scss @@ -138,6 +138,15 @@ --theme-button-container-color: #25262A; + --theme-button-attention-gradient: linear-gradient(135deg, #7C3AED, #A855F7); + --theme-button-attention-border: #A855F7; + --theme-button-attention-hover-gradient: linear-gradient(135deg, #8B5CF6, #7C3AED); + --theme-button-attention-hover-border: #7C3AED; + --theme-button-attention-active-gradient: linear-gradient(135deg, #6D28D9, #7C3AED); + --theme-button-attention-hover-shadow: 0 8px 24px rgba(124, 58, 237, 0.4); + --theme-button-attention-active-shadow: 0 4px 12px rgba(124, 58, 237, 0.6); + --theme-button-attention-focus-shadow: 0 0 0 3px rgba(124, 58, 237, 0.3); + --theme-refinput-divider: rgba(255, 255, 255, .07); --theme-refinput-border: rgba(255, 255, 255, .1); @@ -449,6 +458,15 @@ --theme-button-container-color: #F1F1F1; + --theme-button-attention-gradient: linear-gradient(135deg, #EC4899, #F97316); + --theme-button-attention-border: #F97316; + --theme-button-attention-hover-gradient: linear-gradient(135deg, #F97316, #EC4899); + --theme-button-attention-hover-border: #EC4899; + --theme-button-attention-active-gradient: linear-gradient(135deg, #DC2626, #F97316); + --theme-button-attention-hover-shadow: 0 8px 24px rgba(236, 72, 153, 0.3); + --theme-button-attention-active-shadow: 0 4px 12px rgba(236, 72, 153, 0.5); + --theme-button-attention-focus-shadow: 0 0 0 3px rgba(236, 72, 153, 0.2); + --theme-refinput-divider: rgba(0, 0, 0, .07); --theme-refinput-border: rgba(0, 0, 0, .1); diff --git a/packages/theme/styles/button.scss b/packages/theme/styles/button.scss index ebef04acfb..1a39f047cc 100644 --- a/packages/theme/styles/button.scss +++ b/packages/theme/styles/button.scss @@ -169,6 +169,65 @@ } } + &.attention { + position: relative; + overflow: hidden; + font-weight: 600; + border: 2px solid var(--theme-button-attention-border); + background: var(--theme-button-attention-gradient); + animation: attention-pulse 2s ease-in-out infinite; + + .icon { color: var(--button-accent-IconColor); } + span { color: var(--button-accent-LabelColor); } + + &::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: var(--theme-button-attention-shine); + transition: left 0.6s ease; + } + + &:not(.disabled, :disabled):hover { + background: var(--theme-button-attention-hover-gradient); + border-color: var(--theme-button-attention-hover-border); + transform: translateY(-2px); + box-shadow: var(--theme-button-attention-hover-shadow); + + &::before { left: 100%; } + } + + &:not(.disabled, :disabled):active, + &.pressed:not(.disabled, :disabled) { + transform: translateY(0) scale(0.98); + box-shadow: var(--theme-button-attention-active-shadow); + } + + &:not(.no-focus):focus { + box-shadow: var(--theme-button-attention-focus-shadow); + } + + &:disabled:not(.loading), + &.disabled:not(.loading) { + background: var(--button-disabled-BackgroundColor); + border-color: var(--button-disabled-BackgroundColor); + animation: none; + transform: none; + box-shadow: none; + + &::before { display: none; } + } + + &.loading { + background: var(--theme-button-attention-active-gradient); + + span { color: var(--button-accent-LabelColor); } + } + } + & > * { pointer-events: none; } } @@ -532,6 +591,63 @@ background-color: var(--negative-button-disabled); } } + &.attention { + position: relative; + overflow: hidden; + font-weight: 600; + color: var(--primary-button-color); + background: var(--theme-button-attention-gradient); + border: 2px solid var(--theme-button-attention-border); + animation: attention-pulse 2s ease-in-out infinite; + + .btn-icon, + .btn-right-icon { color: var(--primary-button-color); } + + &::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: var(--theme-button-attention-shine); + transition: left 0.6s ease; + } + + &:hover { + background: var(--theme-button-attention-hover-gradient); + border-color: var(--theme-button-attention-hover-border); + transform: translateY(-2px); + box-shadow: var(--theme-button-attention-hover-shadow); + + &::before { left: 100%; } + } + + &:active, + &.pressed, + &.pressed:hover { + transform: translateY(0) scale(0.98); + box-shadow: var(--theme-button-attention-active-shadow); + } + + &:not(.no-focus):focus { + box-shadow: var(--theme-button-attention-focus-shadow); + } + + &:disabled { + color: var(--primary-button-disabled-color); + background: var(--primary-button-disabled); + border-color: var(--primary-button-disabled); + animation: none; + transform: none; + box-shadow: none; + + &::before { display: none; } + + .btn-icon, + .btn-right-icon { color: var(--primary-button-disabled-color); } + } + } &.contrast { padding: .75rem 1rem; font-weight: 500; @@ -902,4 +1018,15 @@ content: none; } } + + @keyframes attention-pulse { + 0%, 100% { + box-shadow: var(--theme-button-attention-active-shadow), 0 0 0 0 var(--theme-button-attention-border); + transform: scale(1); + } + 50% { + box-shadow: var(--theme-button-attention-hover-shadow), 0 0 0 4px rgba(124, 58, 237, 0.1); + transform: scale(1.02); + } + } } diff --git a/packages/ui/src/components/Button.svelte b/packages/ui/src/components/Button.svelte index 571554a874..b29c11a479 100644 --- a/packages/ui/src/components/Button.svelte +++ b/packages/ui/src/components/Button.svelte @@ -79,7 +79,7 @@ label === undefined && $$slots.content === undefined && (icon !== undefined || iconRight !== undefined || $$slots.icon || $$slots.iconRight) - $: primary = ['primary', 'secondary', 'positive', 'negative'].some((p) => p === kind) + $: primary = ['primary', 'secondary', 'positive', 'negative', 'attention'].some((p) => p === kind) $: devSize = $deviceInfo.size $: adaptive = adaptiveShrink !== null ? checkAdaptiveMatching(devSize, adaptiveShrink) : false diff --git a/packages/ui/src/components/ModernDialog.svelte b/packages/ui/src/components/ModernDialog.svelte index 5d31fd060e..1518a64e27 100644 --- a/packages/ui/src/components/ModernDialog.svelte +++ b/packages/ui/src/components/ModernDialog.svelte @@ -174,6 +174,10 @@ max-height: unset; border-radius: 0; } + + &.extendedHeight { + max-height: 90vh; + } } .shadow { diff --git a/packages/ui/src/types.ts b/packages/ui/src/types.ts index b87e52f9cf..c45f2af041 100644 --- a/packages/ui/src/types.ts +++ b/packages/ui/src/types.ts @@ -174,6 +174,7 @@ export type ButtonKind = | 'list-header' | 'contrast' | 'stepper' + | 'attention' export type ButtonSize = 'inline' | 'x-small' | 'small' | 'medium' | 'large' | 'x-large' export type ButtonShape = | 'rectangle' diff --git a/plugins/billing-assets/lang/cs.json b/plugins/billing-assets/lang/cs.json index 78f4991b36..e3f855d264 100644 --- a/plugins/billing-assets/lang/cs.json +++ b/plugins/billing-assets/lang/cs.json @@ -53,6 +53,9 @@ "StorageUsage": "Úložiště", "TrafficUsage": "Video/audio provoz", "Usage": "Využití", - "Of": "z" + "Of": "z", + "UpgradePlan": "Upgradovat plán", + "LimitReached": "Dosažen limit pracovního prostoru, některé funkce mohou být zakázány. Upgradujte pro obnovení plného přístupu.", + "AskBillingAdmin": "Kontaktujte prosím svého správce fakturace pro upgrade plánu." } } diff --git a/plugins/billing-assets/lang/de.json b/plugins/billing-assets/lang/de.json index 653198348c..12b08e7899 100644 --- a/plugins/billing-assets/lang/de.json +++ b/plugins/billing-assets/lang/de.json @@ -53,6 +53,9 @@ "StorageUsage": "Speicher", "TrafficUsage": "Video/Audio-Traffic", "Usage": "Nutzung", - "Of": "von" + "Of": "von", + "UpgradePlan": "Plan upgraden", + "LimitReached": "Arbeitsbereich-Limit erreicht, einige Funktionen könnten deaktiviert sein. Upgraden Sie für vollen Zugriff.", + "AskBillingAdmin": "Bitte kontaktieren Sie Ihren Abrechnungsadministrator, um den Plan zu upgraden." } } diff --git a/plugins/billing-assets/lang/en.json b/plugins/billing-assets/lang/en.json index aa96ce2c12..a50044cdbc 100644 --- a/plugins/billing-assets/lang/en.json +++ b/plugins/billing-assets/lang/en.json @@ -53,6 +53,9 @@ "StorageUsage": "Storage", "TrafficUsage": "Video/Audio traffic", "Usage": "Usage", - "Of": "of" + "Of": "of", + "UpgradePlan": "Upgrade Plan", + "LimitReached": "Workspace limit reached, some functionality may be disabled. Upgrade to restore full access.", + "AskBillingAdmin": "Please contact your billing administrator to upgrade the plan." } } diff --git a/plugins/billing-assets/lang/es.json b/plugins/billing-assets/lang/es.json index 4760188bd1..5d1d65e611 100644 --- a/plugins/billing-assets/lang/es.json +++ b/plugins/billing-assets/lang/es.json @@ -53,6 +53,9 @@ "StorageUsage": "Almacenamiento", "TrafficUsage": "Tráfico de vídeo/audio", "Usage": "Uso", - "Of": "de" + "Of": "de", + "UpgradePlan": "Actualizar Plan", + "LimitReached": "Límite del espacio de trabajo alcanzado, algunas funciones pueden estar deshabilitadas. Actualiza para restaurar acceso completo.", + "AskBillingAdmin": "Por favor, contacta a tu administrador de facturación para actualizar el plan." } } diff --git a/plugins/billing-assets/lang/fr.json b/plugins/billing-assets/lang/fr.json index 37745ab6d0..176970c60b 100644 --- a/plugins/billing-assets/lang/fr.json +++ b/plugins/billing-assets/lang/fr.json @@ -53,6 +53,9 @@ "StorageUsage": "Stockage", "TrafficUsage": "Trafic vidéo/audio", "Usage": "Utilisation", - "Of": "de" + "Of": "de", + "UpgradePlan": "Mettre à niveau le plan", + "LimitReached": "Limite de l'espace de travail atteinte, certaines fonctionnalités peuvent être désactivées. Mettez à niveau pour restaurer l'accès complet.", + "AskBillingAdmin": "Veuillez contacter votre administrateur de facturation pour mettre à niveau le plan." } } diff --git a/plugins/billing-assets/lang/it.json b/plugins/billing-assets/lang/it.json index 4fa4bbe028..08312702bf 100644 --- a/plugins/billing-assets/lang/it.json +++ b/plugins/billing-assets/lang/it.json @@ -53,6 +53,9 @@ "StorageUsage": "Archiviazione", "TrafficUsage": "Traffico video/audio", "Usage": "Utilizzo", - "Of": "di" + "Of": "di", + "UpgradePlan": "Aggiorna Piano", + "LimitReached": "Limite workspace raggiunto, alcune funzionalità potrebbero essere disabilitate. Aggiorna per ripristinare l'accesso completo.", + "AskBillingAdmin": "Contatta il tuo amministratore di fatturazione per aggiornare il piano." } } diff --git a/plugins/billing-assets/lang/ja.json b/plugins/billing-assets/lang/ja.json index ee2ea89a55..5adad96674 100644 --- a/plugins/billing-assets/lang/ja.json +++ b/plugins/billing-assets/lang/ja.json @@ -53,6 +53,9 @@ "StorageUsage": "ストレージ", "TrafficUsage": "ビデオ/オーディオトラフィック", "Usage": "使用状況", - "Of": "/" + "Of": "/", + "UpgradePlan": "プランをアップグレード", + "LimitReached": "ワークスペースの制限に達しました。一部の機能が無効になる場合があります。アップグレードして完全アクセスを復元してください。", + "AskBillingAdmin": "プランをアップグレードするには、請求管理者にお問い合わせください。" } } diff --git a/plugins/billing-assets/lang/pt.json b/plugins/billing-assets/lang/pt.json index 22324145fb..c7bc9cade9 100644 --- a/plugins/billing-assets/lang/pt.json +++ b/plugins/billing-assets/lang/pt.json @@ -53,6 +53,9 @@ "StorageUsage": "Armazenamento", "TrafficUsage": "Tráfego de vídeo/áudio", "Usage": "Uso", - "Of": "de" + "Of": "de", + "UpgradePlan": "Atualizar Plano", + "LimitReached": "Limite do workspace atingido, algumas funcionalidades podem estar desabilitadas. Atualize para restaurar acesso completo.", + "AskBillingAdmin": "Por favor, contate seu administrador de faturamento para atualizar o plano." } } diff --git a/plugins/billing-assets/lang/ru.json b/plugins/billing-assets/lang/ru.json index 594cd3a1db..da7db87db5 100644 --- a/plugins/billing-assets/lang/ru.json +++ b/plugins/billing-assets/lang/ru.json @@ -53,6 +53,9 @@ "StorageUsage": "Хранилище", "TrafficUsage": "Видео/аудио трафик", "Usage": "Использование", - "Of": "из" + "Of": "из", + "UpgradePlan": "Обновить план", + "LimitReached": "Достигнут лимит рабочего пространства, некоторые функции могут быть отключены. Обновите план для восстановления полного доступа.", + "AskBillingAdmin": "Пожалуйста, свяжитесь с вашим администратором биллинга для обновления плана." } } diff --git a/plugins/billing-assets/lang/tr.json b/plugins/billing-assets/lang/tr.json index 8c63ab1000..176b84de45 100644 --- a/plugins/billing-assets/lang/tr.json +++ b/plugins/billing-assets/lang/tr.json @@ -53,6 +53,9 @@ "StorageUsage": "Depolama", "TrafficUsage": "Video/ses trafiği", "Usage": "Kullanım", - "Of": "/" + "Of": "/", + "UpgradePlan": "Planı Yükselt", + "LimitReached": "Çalışma alanı limiti aşıldı, bazı işlevler devre dışı bırakılabilir. Tam erişimi geri yüklemek için yükseltin.", + "AskBillingAdmin": "Lütfen planı yükseltmek için faturalama yöneticinizle iletişime geçin." } } diff --git a/plugins/billing-assets/lang/zh.json b/plugins/billing-assets/lang/zh.json index 09df476a37..3755b8170f 100644 --- a/plugins/billing-assets/lang/zh.json +++ b/plugins/billing-assets/lang/zh.json @@ -53,6 +53,9 @@ "StorageUsage": "存储", "TrafficUsage": "视频/音频流量", "Usage": "使用情况", - "Of": "/" + "Of": "/", + "UpgradePlan": "升级计划", + "LimitReached": "已达到工作空间限制,某些功能可能被禁用。请升级以恢复完全访问权限。", + "AskBillingAdmin": "请联系您的计费管理员以升级计划。" } } diff --git a/plugins/billing-resources/src/components/SubscriptionsModal.svelte b/plugins/billing-resources/src/components/SubscriptionsModal.svelte new file mode 100644 index 0000000000..b3b9dc2a4d --- /dev/null +++ b/plugins/billing-resources/src/components/SubscriptionsModal.svelte @@ -0,0 +1,23 @@ + + + + + + diff --git a/plugins/billing-resources/src/components/UpgradeButton.svelte b/plugins/billing-resources/src/components/UpgradeButton.svelte new file mode 100644 index 0000000000..04b66ab2b1 --- /dev/null +++ b/plugins/billing-resources/src/components/UpgradeButton.svelte @@ -0,0 +1,75 @@ + + + +{#if limitExceeded} +
+
+{/if} diff --git a/plugins/billing-resources/src/components/WorkbenchExtension.svelte b/plugins/billing-resources/src/components/WorkbenchExtension.svelte new file mode 100644 index 0000000000..80a459a7d8 --- /dev/null +++ b/plugins/billing-resources/src/components/WorkbenchExtension.svelte @@ -0,0 +1,32 @@ + + + +