From 7ac9a3dbd327b292fad1198ed0deacf8a6cb9f36 Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Sun, 26 Apr 2026 21:17:23 +0300 Subject: [PATCH] =?UTF-8?q?UI=20-=20=D0=9C=D0=95=D0=96=D0=9F=D0=A0=D0=9E?= =?UTF-8?q?=D0=95=D0=9A=D0=A2=D0=9D=D0=90=D0=AF=20=D0=9A=D0=9E=D0=9C=D0=9C?= =?UTF-8?q?=D0=A3=D0=9D=D0=98=D0=9A=D0=90=D0=A6=D0=98=D0=AF:=20=D1=84?= =?UTF-8?q?=D0=B8=D0=BB=D1=8C=D1=82=D1=80=D1=8B=20=D0=B0=D0=BA=D1=82=D0=B8?= =?UTF-8?q?=D0=B2=D0=BD=D0=BE=D1=81=D1=82=D0=B8=20=D0=B8=20=D0=B5=D0=B4?= =?UTF-8?q?=D0=B8=D0=BD=D1=8B=D0=B9=20voice=20loader?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/components/common/logo-spinner.tsx | 11 +- .../common/nodedc-processing-loader.tsx | 29 +++ .../components/home/widgets/recents/index.tsx | 189 +++++++++++++++++- .../voice-tasker/global-control.tsx | 11 +- .../core/lib/b-progress/AppProgressBar.tsx | 16 +- .../lib/vendor/react-audio-visualize/index.ts | 1 + .../live-audio-visualizer.tsx | 172 ++++++++++++++++ plane-src/apps/web/package.json | 1 - plane-src/apps/web/styles/globals.css | 105 +++++++++- plane-src/pnpm-lock.yaml | 14 -- 10 files changed, 509 insertions(+), 40 deletions(-) create mode 100644 plane-src/apps/web/core/components/common/nodedc-processing-loader.tsx create mode 100644 plane-src/apps/web/core/lib/vendor/react-audio-visualize/index.ts create mode 100644 plane-src/apps/web/core/lib/vendor/react-audio-visualize/live-audio-visualizer.tsx diff --git a/plane-src/apps/web/core/components/common/logo-spinner.tsx b/plane-src/apps/web/core/components/common/logo-spinner.tsx index 9dfad1d..1c80f6f 100644 --- a/plane-src/apps/web/core/components/common/logo-spinner.tsx +++ b/plane-src/apps/web/core/components/common/logo-spinner.tsx @@ -4,19 +4,12 @@ * See the LICENSE file for details. */ -import { useTheme } from "next-themes"; -// assets -import LogoSpinnerDark from "@/app/assets/images/logo-spinner-dark.gif?url"; -import LogoSpinnerLight from "@/app/assets/images/logo-spinner-light.gif?url"; +import { NodedcProcessingLoader } from "./nodedc-processing-loader"; export function LogoSpinner() { - const { resolvedTheme } = useTheme(); - - const logoSrc = resolvedTheme === "dark" ? LogoSpinnerDark : LogoSpinnerLight; - return (
- logo +
); } diff --git a/plane-src/apps/web/core/components/common/nodedc-processing-loader.tsx b/plane-src/apps/web/core/components/common/nodedc-processing-loader.tsx new file mode 100644 index 0000000..c6b6de3 --- /dev/null +++ b/plane-src/apps/web/core/components/common/nodedc-processing-loader.tsx @@ -0,0 +1,29 @@ +import { cn } from "@plane/utils"; + +type TNodedcProcessingLoaderProps = { + className?: string; + label?: string; + tone?: "accent" | "white"; + variant?: "orbit" | "fluid"; +}; + +export function NodedcProcessingLoader({ + className, + label, + tone = "accent", + variant = "orbit", +}: TNodedcProcessingLoaderProps) { + return ( +
+
+ ); +} diff --git a/plane-src/apps/web/core/components/home/widgets/recents/index.tsx b/plane-src/apps/web/core/components/home/widgets/recents/index.tsx index 2cd75dd..32b8873 100644 --- a/plane-src/apps/web/core/components/home/widgets/recents/index.tsx +++ b/plane-src/apps/web/core/components/home/widgets/recents/index.tsx @@ -6,13 +6,14 @@ import { useMemo, useRef, useState } from "react"; import { observer } from "mobx-react"; +import { ArrowDownUp, Check, Filter } from "lucide-react"; import useSWR from "swr"; import { useTranslation } from "@plane/i18n"; // plane types import { PageIcon, ProjectIcon, WorkItemsIcon } from "@plane/propel/icons"; import { Avatar } from "@plane/propel/avatar"; import type { IIssueActivity, TActivityEntityData, THomeWidgetProps, TRecentActivityFilterKeys } from "@plane/types"; -import { calculateTimeAgo, generateWorkItemLink, getFileURL, renderFormattedDate } from "@plane/utils"; +import { calculateTimeAgo, cn, generateWorkItemLink, getFileURL, renderFormattedDate } from "@plane/utils"; import { ActivityIcon } from "@/components/core/activity"; // plane web services import { WorkspaceService } from "@/services/workspace.service"; @@ -34,6 +35,35 @@ const filters: { name: TRecentActivityFilterKeys; icon?: React.ReactNode; i18n_k { name: "project", icon: , i18n_key: "home.recents.filters.projects" }, ]; +type TActivityTraceFilterKey = + | "archive" + | "assignees" + | "content" + | "created" + | "dates" + | "labels" + | "mine" + | "priority" + | "state"; +type TActivityTraceSortMode = "newest" | "oldest"; + +const ACTIVITY_TRACE_FILTER_OPTIONS: { key: TActivityTraceFilterKey; label: string }[] = [ + { key: "created", label: "Новые задачи" }, + { key: "assignees", label: "Исполнители" }, + { key: "state", label: "Статусы" }, + { key: "priority", label: "Приоритеты" }, + { key: "dates", label: "Сроки и даты" }, + { key: "content", label: "Название и описание" }, + { key: "labels", label: "Метки" }, + { key: "archive", label: "Архив и восстановление" }, + { key: "mine", label: "Только мои действия" }, +]; + +const ACTIVITY_TRACE_SORT_OPTIONS: { key: TActivityTraceSortMode; label: string }[] = [ + { key: "newest", label: "Новые сверху" }, + { key: "oldest", label: "Старые сверху" }, +]; + const PRIORITY_LABELS: Record = { urgent: "Срочный", high: "Высокий", @@ -83,6 +113,39 @@ const activityMessage = (activity: IIssueActivity) => { } }; +const getActivityTraceFilterKeys = (activity: IIssueActivity): TActivityTraceFilterKey[] => { + if (!activity.field && activity.verb === "created") return ["created"]; + + switch (activity.field) { + case "assignees": + return ["assignees"]; + case "state": + return ["state"]; + case "priority": + return ["priority"]; + case "start_date": + case "target_date": + return ["dates"]; + case "name": + case "description": + return ["content"]; + case "labels": + return ["labels"]; + case "archived_at": + return ["archive"]; + default: + return []; + } +}; + +const sortActivityTrace = (activities: IIssueActivity[], sortMode: TActivityTraceSortMode) => + [...activities].sort((firstActivity, secondActivity) => { + const firstTime = new Date(firstActivity.created_at).getTime(); + const secondTime = new Date(secondActivity.created_at).getTime(); + + return sortMode === "oldest" ? firstTime - secondTime : secondTime - firstTime; + }); + type TRecentWidgetProps = THomeWidgetProps & { presetFilter?: TRecentActivityFilterKeys; showFilterSelect?: boolean; @@ -158,6 +221,9 @@ export const RecentActivityWidget = observer(function RecentActivityWidget(props const { presetFilter, showFilterSelect = true, workspaceSlug, projectId, recents: preloadedRecents } = props; // states const [filter, setFilter] = useState(presetFilter ?? filters[0].name); + const [activeTraceFilters, setActiveTraceFilters] = useState([]); + const [isTraceFilterOpen, setIsTraceFilterOpen] = useState(false); + const [traceSortMode, setTraceSortMode] = useState("newest"); const { t } = useTranslation(); const { data: currentUser } = useUser(); // ref @@ -171,7 +237,7 @@ export const RecentActivityWidget = observer(function RecentActivityWidget(props workspaceSlug ? () => workspaceService.fetchWorkspaceActivity(workspaceSlug.toString(), { - per_page: 20, + per_page: 60, ...(projectId ? { project: projectId } : {}), }) : null, @@ -200,7 +266,20 @@ export const RecentActivityWidget = observer(function RecentActivityWidget(props } ); - const activityTrace = useMemo(() => fetchedActivity?.results ?? [], [fetchedActivity?.results]); + const activityTrace = useMemo(() => { + const source = fetchedActivity?.results ?? []; + const onlyMine = activeTraceFilters.includes("mine"); + const fieldFilters = activeTraceFilters.filter((filterKey) => filterKey !== "mine"); + const filteredTrace = source.filter((activity) => { + if (onlyMine && currentUser?.id !== activity.actor_detail?.id) return false; + if (fieldFilters.length === 0) return true; + + const activityFilterKeys = getActivityTraceFilterKeys(activity); + return fieldFilters.some((filterKey) => activityFilterKeys.includes(filterKey)); + }); + + return sortActivityTrace(filteredTrace, traceSortMode); + }, [activeTraceFilters, currentUser?.id, fetchedActivity?.results, traceSortMode]); const recents = useMemo(() => { const source = preloadedRecents ?? fetchedRecents ?? []; @@ -217,6 +296,106 @@ export const RecentActivityWidget = observer(function RecentActivityWidget(props const isWidgetLoading = shouldUseActivityTrace ? isActivityLoading : isLoading; const isWidgetEmpty = shouldUseActivityTrace ? activityTrace.length === 0 : recents.length === 0; + const activeTraceFilterCount = activeTraceFilters.length + (traceSortMode === "newest" ? 0 : 1); + + const toggleTraceFilter = (filterKey: TActivityTraceFilterKey) => + setActiveTraceFilters((currentFilters) => + currentFilters.includes(filterKey) + ? currentFilters.filter((currentFilter) => currentFilter !== filterKey) + : [...currentFilters, filterKey] + ); + + const resetTraceFilters = () => { + setActiveTraceFilters([]); + setTraceSortMode("newest"); + }; + + const headerActions = ( +
+ {showFilterSelect && } + {shouldUseActivityTrace && ( +
+ + + {isTraceFilterOpen && ( +
+
+
Тип действия
+ {ACTIVITY_TRACE_FILTER_OPTIONS.map((option) => { + const isActive = activeTraceFilters.includes(option.key); + + return ( + + ); + })} +
+ +
+
Сортировка
+ {ACTIVITY_TRACE_SORT_OPTIONS.map((option) => { + const isActive = traceSortMode === option.key; + + return ( + + ); + })} +
+ + {activeTraceFilterCount > 0 && ( + + )} +
+ )} +
+ )} +
+ ); const resolveRecent = (activity: TActivityEntityData) => { switch (activity.entity_name) { @@ -237,7 +416,7 @@ export const RecentActivityWidget = observer(function RecentActivityWidget(props
{t("home.recents.title")}
- {showFilterSelect && } + {headerActions}
@@ -249,7 +428,7 @@ export const RecentActivityWidget = observer(function RecentActivityWidget(props
{t("home.recents.title")}
- {showFilterSelect && } + {headerActions}
{isWidgetLoading && } diff --git a/plane-src/apps/web/core/components/voice-tasker/global-control.tsx b/plane-src/apps/web/core/components/voice-tasker/global-control.tsx index 47d1cb3..7ad127c 100644 --- a/plane-src/apps/web/core/components/voice-tasker/global-control.tsx +++ b/plane-src/apps/web/core/components/voice-tasker/global-control.tsx @@ -7,7 +7,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import type { ElementType, MouseEvent, ReactNode } from "react"; import { useParams } from "next/navigation"; -import { LiveAudioVisualizer } from "react-audio-visualize"; import useSWR from "swr"; import { AudioLines, @@ -51,9 +50,11 @@ import { MemberDropdown } from "@/components/dropdowns/member/dropdown"; import { PriorityDropdown } from "@/components/dropdowns/priority"; import { ProjectDropdown } from "@/components/dropdowns/project/dropdown"; import { StateDropdown } from "@/components/dropdowns/state/dropdown"; +import { NodedcProcessingLoader } from "@/components/common/nodedc-processing-loader"; import { IssueIdentifier } from "@/plane-web/components/issues/issue-details/issue-identifier"; import { useIssues } from "@/hooks/store/use-issues"; import useDebounce from "@/hooks/use-debounce"; +import { LiveAudioVisualizer } from "@/lib/vendor/react-audio-visualize"; // services import { ProjectService } from "@/services/project"; import { WorkspaceAIService } from "@/services/workspace-ai.service"; @@ -442,8 +443,8 @@ function VoiceTaskWaveform({ mediaRecorder }: { mediaRecorder: MediaRecorder | n barColor={accentColor} fftSize={1024} minDecibels={-92} - maxDecibels={-18} - smoothingTimeConstant={0.68} + maxDecibels={-30} + smoothingTimeConstant={0.52} />
@@ -452,8 +453,8 @@ function VoiceTaskWaveform({ mediaRecorder }: { mediaRecorder: MediaRecorder | n function VoiceTaskProcessingState() { return ( -
-