ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: профиль, редиректы и визуальные статусы задач
This commit is contained in:
parent
c4032e3040
commit
4ed63cac4e
|
|
@ -48,9 +48,9 @@ x-live-env: &live-env
|
||||||
LIVE_SERVER_SECRET_KEY: ${LIVE_SERVER_SECRET_KEY:-2FiJk1U2aiVPEQtzLehYGlTSnTnrs7LW}
|
LIVE_SERVER_SECRET_KEY: ${LIVE_SERVER_SECRET_KEY:-2FiJk1U2aiVPEQtzLehYGlTSnTnrs7LW}
|
||||||
|
|
||||||
x-app-env: &app-env
|
x-app-env: &app-env
|
||||||
WEB_URL: ${WEB_URL:-http://localhost}
|
WEB_URL: ${WEB_URL:-http://localhost:8090}
|
||||||
DEBUG: ${DEBUG:-0}
|
DEBUG: ${DEBUG:-0}
|
||||||
CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS}
|
CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS:-http://localhost:8090}
|
||||||
GUNICORN_WORKERS: 1
|
GUNICORN_WORKERS: 1
|
||||||
POSTHOG_API_KEY: ${POSTHOG_API_KEY:-}
|
POSTHOG_API_KEY: ${POSTHOG_API_KEY:-}
|
||||||
POSTHOG_HOST: ${POSTHOG_HOST:-}
|
POSTHOG_HOST: ${POSTHOG_HOST:-}
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,11 @@ urlpatterns = [
|
||||||
UserEndpoint.as_view({"patch": "update_email"}),
|
UserEndpoint.as_view({"patch": "update_email"}),
|
||||||
name="user-email-update",
|
name="user-email-update",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"users/me/email/direct/",
|
||||||
|
UserEndpoint.as_view({"patch": "update_email_without_verification"}),
|
||||||
|
name="user-email-direct-update",
|
||||||
|
),
|
||||||
# Profile
|
# Profile
|
||||||
path("users/me/profile/", ProfileEndpoint.as_view(), name="accounts"),
|
path("users/me/profile/", ProfileEndpoint.as_view(), name="accounts"),
|
||||||
# End profile
|
# End profile
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
# See the LICENSE file for details.
|
# See the LICENSE file for details.
|
||||||
|
|
||||||
# Python imports
|
# Python imports
|
||||||
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
|
@ -50,6 +51,7 @@ from plane.bgtasks.user_deactivation_email_task import user_deactivation_email
|
||||||
from plane.utils.host import base_host
|
from plane.utils.host import base_host
|
||||||
from plane.bgtasks.user_email_update_task import send_email_update_magic_code, send_email_update_confirmation
|
from plane.bgtasks.user_email_update_task import send_email_update_magic_code, send_email_update_confirmation
|
||||||
from plane.authentication.rate_limit import EmailVerificationThrottle
|
from plane.authentication.rate_limit import EmailVerificationThrottle
|
||||||
|
from plane.license.utils.instance_value import get_configuration_value
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger("plane")
|
logger = logging.getLogger("plane")
|
||||||
|
|
@ -133,6 +135,10 @@ class UserEndpoint(BaseViewSet):
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _is_smtp_configured(self):
|
||||||
|
(email_host,) = get_configuration_value([{"key": "EMAIL_HOST", "default": os.environ.get("EMAIL_HOST", "")}])
|
||||||
|
return bool(email_host)
|
||||||
|
|
||||||
def generate_email_verification_code(self, request):
|
def generate_email_verification_code(self, request):
|
||||||
"""
|
"""
|
||||||
Generate and send a magic code to the new email address for verification.
|
Generate and send a magic code to the new email address for verification.
|
||||||
|
|
@ -248,6 +254,33 @@ class UserEndpoint(BaseViewSet):
|
||||||
serialized_data = UserMeSerializer(user).data
|
serialized_data = UserMeSerializer(user).data
|
||||||
return Response(serialized_data, status=status.HTTP_200_OK)
|
return Response(serialized_data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
def update_email_without_verification(self, request):
|
||||||
|
"""
|
||||||
|
Update the current user's email when the instance has no SMTP configured.
|
||||||
|
Verified SMTP-backed installations must continue using the magic-code flow.
|
||||||
|
"""
|
||||||
|
if self._is_smtp_configured():
|
||||||
|
return Response(
|
||||||
|
{"error": "Email verification is required when SMTP is configured"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
user = self.get_object()
|
||||||
|
new_email = request.data.get("email", "").strip().lower()
|
||||||
|
|
||||||
|
validation_error = self._validate_new_email(user, new_email)
|
||||||
|
if validation_error:
|
||||||
|
return validation_error
|
||||||
|
|
||||||
|
user.email = new_email
|
||||||
|
user.is_email_verified = False
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
logout(request)
|
||||||
|
|
||||||
|
serialized_data = UserMeSerializer(user).data
|
||||||
|
return Response(serialized_data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def deactivate(self, request):
|
def deactivate(self, request):
|
||||||
# Check all workspace user is active
|
# Check all workspace user is active
|
||||||
user = self.get_object()
|
user = self.get_object()
|
||||||
|
|
|
||||||
|
|
@ -219,6 +219,7 @@ export const ExternalContoursBoardItem = observer(function ExternalContoursBoard
|
||||||
<div className="group/kanban-block relative mb-2">
|
<div className="group/kanban-block relative mb-2">
|
||||||
<div
|
<div
|
||||||
data-active={isActive}
|
data-active={isActive}
|
||||||
|
data-priority={issue.priority ?? "none"}
|
||||||
className="nodedc-external-card relative flex min-h-[220px] w-full cursor-pointer flex-col p-4 transition-all hover:bg-white/5"
|
className="nodedc-external-card relative flex min-h-[220px] w-full cursor-pointer flex-col p-4 transition-all hover:bg-white/5"
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ export const ExternalContoursListItem = observer(function ExternalContoursListIt
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
data-active={isActive}
|
data-active={isActive}
|
||||||
|
data-priority={issue.priority ?? "none"}
|
||||||
className={cn(
|
className={cn(
|
||||||
"nodedc-external-card relative flex min-h-[15rem] cursor-pointer flex-col gap-5 px-6 py-5 transition-all hover:bg-white/5",
|
"nodedc-external-card relative flex min-h-[15rem] cursor-pointer flex-col gap-5 px-6 py-5 transition-all hover:bg-white/5",
|
||||||
{ "ring-0": isActive }
|
{ "ring-0": isActive }
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ import { useUser } from "@/hooks/store/user";
|
||||||
import { AuthService } from "@/services/auth.service";
|
import { AuthService } from "@/services/auth.service";
|
||||||
import userService from "@/services/user.service";
|
import userService from "@/services/user.service";
|
||||||
|
|
||||||
type Props = { isOpen: boolean; onClose: () => void };
|
type Props = { isOpen: boolean; onClose: () => void; isSMTPConfigured?: boolean };
|
||||||
|
|
||||||
type TModalStep = "EMAIL" | "UNIQUE_CODE";
|
type TModalStep = "EMAIL" | "UNIQUE_CODE";
|
||||||
type TUniqueCodeValuesForm = { email: string; code: string };
|
type TUniqueCodeValuesForm = { email: string; code: string };
|
||||||
|
|
@ -33,7 +33,7 @@ const defaultValues: TUniqueCodeValuesForm = { email: "", code: "" };
|
||||||
const authService = new AuthService();
|
const authService = new AuthService();
|
||||||
|
|
||||||
export const ChangeEmailModal = observer(function ChangeEmailModal(props: Props) {
|
export const ChangeEmailModal = observer(function ChangeEmailModal(props: Props) {
|
||||||
const { isOpen, onClose } = props;
|
const { isOpen, onClose, isSMTPConfigured = true } = props;
|
||||||
// states
|
// states
|
||||||
const [currentStep, setCurrentStep] = useState<TModalStep>("EMAIL");
|
const [currentStep, setCurrentStep] = useState<TModalStep>("EMAIL");
|
||||||
// store hooks
|
// store hooks
|
||||||
|
|
@ -107,6 +107,20 @@ export const ChangeEmailModal = observer(function ChangeEmailModal(props: Props)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isSMTPConfigured) {
|
||||||
|
await userService.updateEmailDirect({ email: formData.email });
|
||||||
|
|
||||||
|
setToast({
|
||||||
|
type: TOAST_TYPE.SUCCESS,
|
||||||
|
title: changeEmailT("toasts.success_title"),
|
||||||
|
message: changeEmailT("toasts.success_message"),
|
||||||
|
});
|
||||||
|
|
||||||
|
await handleSignOut();
|
||||||
|
handleClose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Generate verification code and send to new email
|
// Generate verification code and send to new email
|
||||||
await userService.generateEmailCode({ email: formData.email });
|
await userService.generateEmailCode({ email: formData.email });
|
||||||
|
|
||||||
|
|
@ -135,7 +149,9 @@ export const ChangeEmailModal = observer(function ChangeEmailModal(props: Props)
|
||||||
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.XXL}>
|
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.XXL}>
|
||||||
<div className="space-y-0 px-4 py-4">
|
<div className="space-y-0 px-4 py-4">
|
||||||
<h3 className="text-16 leading-6 font-medium text-primary">{changeEmailT("title")}</h3>
|
<h3 className="text-16 leading-6 font-medium text-primary">{changeEmailT("title")}</h3>
|
||||||
<p className="my-4 text-13 text-secondary">{changeEmailT("description")}</p>
|
<p className="my-4 text-13 text-secondary">
|
||||||
|
{isSMTPConfigured ? changeEmailT("description") : changeEmailT("direct_description")}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4 px-4" noValidate>
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4 px-4" noValidate>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
|
|
|
||||||
|
|
@ -225,6 +225,7 @@ const HomeInternalContourDeckCard = observer(function HomeInternalContourDeckCar
|
||||||
>
|
>
|
||||||
<NodedcWorkItemCard
|
<NodedcWorkItemCard
|
||||||
isActive={isActive}
|
isActive={isActive}
|
||||||
|
priority={issue.priority}
|
||||||
surfaceClassName={cn(
|
surfaceClassName={cn(
|
||||||
"nodedc-home-task-card-surface px-0",
|
"nodedc-home-task-card-surface px-0",
|
||||||
compact && "!rounded-[24px]",
|
compact && "!rounded-[24px]",
|
||||||
|
|
@ -276,6 +277,7 @@ const HomeExternalContourDeckCard = observer(function HomeExternalContourDeckCar
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
data-active={isActive}
|
data-active={isActive}
|
||||||
|
data-priority={issue.priority ?? "none"}
|
||||||
className={cn(
|
className={cn(
|
||||||
"nodedc-external-card nodedc-home-task-card-surface relative flex w-full flex-col",
|
"nodedc-external-card nodedc-home-task-card-surface relative flex w-full flex-col",
|
||||||
compact ? "min-h-[168px] rounded-[24px] p-3" : "min-h-[220px] p-4",
|
compact ? "min-h-[168px] rounded-[24px] p-3" : "min-h-[220px] p-4",
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,7 @@ export const InboxIssueListItem = observer(function InboxIssueListItem(props: In
|
||||||
>
|
>
|
||||||
<NodedcWorkItemCard
|
<NodedcWorkItemCard
|
||||||
isActive={isActive}
|
isActive={isActive}
|
||||||
|
priority={issue.priority}
|
||||||
surfaceClassName="transition-transform duration-200 hover:-translate-y-0.5"
|
surfaceClassName="transition-transform duration-200 hover:-translate-y-0.5"
|
||||||
header={
|
header={
|
||||||
<div className="flex items-start justify-between gap-3">
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
|
|
||||||
|
|
@ -215,6 +215,7 @@ export const InternalContourKanbanCard = observer(function InternalContourKanban
|
||||||
return (
|
return (
|
||||||
<NodedcWorkItemCard
|
<NodedcWorkItemCard
|
||||||
isActive={isActive}
|
isActive={isActive}
|
||||||
|
priority={issue.priority}
|
||||||
surfaceClassName="!rounded-[24px] !p-0"
|
surfaceClassName="!rounded-[24px] !p-0"
|
||||||
contentClassName="min-h-[220px]"
|
contentClassName="min-h-[220px]"
|
||||||
header={header}
|
header={header}
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,12 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
import type { TIssuePriorities } from "@plane/types";
|
||||||
import { cn } from "@plane/utils";
|
import { cn } from "@plane/utils";
|
||||||
|
|
||||||
type TNodedcWorkItemCardProps = {
|
type TNodedcWorkItemCardProps = {
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
|
priority?: TIssuePriorities | null;
|
||||||
header: ReactNode;
|
header: ReactNode;
|
||||||
title: ReactNode;
|
title: ReactNode;
|
||||||
footer: ReactNode;
|
footer: ReactNode;
|
||||||
|
|
@ -37,6 +39,7 @@ export const getNodedcWorkItemCardAppearance = (isActive: boolean) => ({
|
||||||
|
|
||||||
export const NodedcWorkItemCard = ({
|
export const NodedcWorkItemCard = ({
|
||||||
isActive,
|
isActive,
|
||||||
|
priority,
|
||||||
header,
|
header,
|
||||||
title,
|
title,
|
||||||
footer,
|
footer,
|
||||||
|
|
@ -52,6 +55,8 @@ export const NodedcWorkItemCard = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
data-active={isActive}
|
||||||
|
data-priority={priority ?? "none"}
|
||||||
className={cn(
|
className={cn(
|
||||||
"nodedc-work-item-card rounded-[28px] border-0 p-4 shadow-none ring-0 outline-none",
|
"nodedc-work-item-card rounded-[28px] border-0 p-4 shadow-none ring-0 outline-none",
|
||||||
appearance.surfaceClassName,
|
appearance.surfaceClassName,
|
||||||
|
|
|
||||||
|
|
@ -191,7 +191,11 @@ export const GeneralProfileSettingsForm = observer(function GeneralProfileSettin
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DeactivateAccountModal isOpen={deactivateAccountModal} onClose={() => setDeactivateAccountModal(false)} />
|
<DeactivateAccountModal isOpen={deactivateAccountModal} onClose={() => setDeactivateAccountModal(false)} />
|
||||||
<ChangeEmailModal isOpen={isChangeEmailModalOpen} onClose={() => setIsChangeEmailModalOpen(false)} />
|
<ChangeEmailModal
|
||||||
|
isOpen={isChangeEmailModalOpen}
|
||||||
|
onClose={() => setIsChangeEmailModalOpen(false)}
|
||||||
|
isSMTPConfigured={isSMTPConfigured}
|
||||||
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="avatar_url"
|
name="avatar_url"
|
||||||
|
|
@ -371,23 +375,19 @@ export const GeneralProfileSettingsForm = observer(function GeneralProfileSettin
|
||||||
ref={ref}
|
ref={ref}
|
||||||
hasError={Boolean(errors.email)}
|
hasError={Boolean(errors.email)}
|
||||||
placeholder={t("profile_general.email_placeholder")}
|
placeholder={t("profile_general.email_placeholder")}
|
||||||
className={`nodedc-settings-input w-full cursor-not-allowed !bg-white/4 ${
|
className={`nodedc-settings-input w-full !bg-white/4 ${errors.email ? "border-danger-strong" : ""}`}
|
||||||
errors.email ? "border-danger-strong" : ""
|
|
||||||
}`}
|
|
||||||
autoComplete="on"
|
autoComplete="on"
|
||||||
disabled
|
disabled
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{isSMTPConfigured && (
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
className="nodedc-settings-chip flex w-fit items-center gap-2 px-3.5 py-1.5 text-12 font-medium text-primary"
|
||||||
className="nodedc-settings-chip flex w-fit items-center gap-2 px-3.5 py-1.5 text-12 font-medium text-primary"
|
onClick={() => setIsChangeEmailModalOpen(true)}
|
||||||
onClick={() => setIsChangeEmailModalOpen(true)}
|
>
|
||||||
>
|
{t("account_settings.profile.change_email_modal.title")}
|
||||||
{t("account_settings.profile.change_email_modal.title")}
|
</button>
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,10 @@ export const ProjectAuthWrapper = observer(function ProjectAuthWrapper(props: IP
|
||||||
() => fetchProjectDetails(workspaceSlug, projectId)
|
() => fetchProjectDetails(workspaceSlug, projectId)
|
||||||
);
|
);
|
||||||
// fetching user project member information
|
// fetching user project member information
|
||||||
useSWR(PROJECT_ME_INFORMATION(workspaceSlug, projectId), () => fetchUserProjectInfo(workspaceSlug, projectId));
|
const { isLoading: isProjectMemberLoading, error: projectMemberError } = useSWR(
|
||||||
|
PROJECT_ME_INFORMATION(workspaceSlug, projectId),
|
||||||
|
() => fetchUserProjectInfo(workspaceSlug, projectId)
|
||||||
|
);
|
||||||
// fetching project member preferences
|
// fetching project member preferences
|
||||||
useSWR(
|
useSWR(
|
||||||
currentUserData?.id ? PROJECT_MEMBER_PREFERENCES(projectId, currentProjectRole) : null,
|
currentUserData?.id ? PROJECT_MEMBER_PREFERENCES(projectId, currentProjectRole) : null,
|
||||||
|
|
@ -142,14 +145,17 @@ export const ProjectAuthWrapper = observer(function ProjectAuthWrapper(props: IP
|
||||||
joinProject(workspaceSlug, projectId).finally(() => setIsJoiningProject(false));
|
joinProject(workspaceSlug, projectId).finally(() => setIsJoiningProject(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
const isProjectLoading = (isParentLoading || isProjectDetailsLoading) && !projectDetailsError;
|
const isProjectLoading =
|
||||||
|
(isParentLoading || isProjectDetailsLoading || isProjectMemberLoading) && !projectDetailsError && !projectMemberError;
|
||||||
|
const accessErrorStatus =
|
||||||
|
projectDetailsError?.status ?? (projectMemberError?.status === 404 ? 403 : projectMemberError?.status);
|
||||||
|
|
||||||
if (isProjectLoading) return null;
|
if (isProjectLoading) return null;
|
||||||
|
|
||||||
if (!isProjectLoading && hasPermissionToCurrentProject === false) {
|
if (!isProjectLoading && hasPermissionToCurrentProject === false) {
|
||||||
return (
|
return (
|
||||||
<ProjectAccessRestriction
|
<ProjectAccessRestriction
|
||||||
errorStatusCode={projectDetailsError?.status}
|
errorStatusCode={accessErrorStatus}
|
||||||
isWorkspaceAdmin={isWorkspaceAdmin}
|
isWorkspaceAdmin={isWorkspaceAdmin}
|
||||||
handleJoinProject={handleJoinProject}
|
handleJoinProject={handleJoinProject}
|
||||||
isJoinButtonDisabled={isJoiningProject}
|
isJoinButtonDisabled={isJoiningProject}
|
||||||
|
|
|
||||||
|
|
@ -297,6 +297,14 @@ export class UserService extends APIService {
|
||||||
throw error?.response?.data;
|
throw error?.response?.data;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateEmailDirect(data: { email: string }): Promise<IUser> {
|
||||||
|
return this.patch("/api/users/me/email/direct/", data)
|
||||||
|
.then((response) => response?.data)
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const userService = new UserService();
|
const userService = new UserService();
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,16 @@
|
||||||
--nodedc-on-card-passive-rgb: 245 247 251;
|
--nodedc-on-card-passive-rgb: 245 247 251;
|
||||||
--nodedc-card-active-rgb: 195 255 102;
|
--nodedc-card-active-rgb: 195 255 102;
|
||||||
--nodedc-on-card-active-rgb: 11 17 23;
|
--nodedc-on-card-active-rgb: 11 17 23;
|
||||||
|
--nodedc-priority-none-rgb: 124 128 138;
|
||||||
|
--nodedc-priority-none-mix-rgb: 255 255 255;
|
||||||
|
--nodedc-priority-low-rgb: 51 163 255;
|
||||||
|
--nodedc-priority-low-mix-rgb: 255 199 95;
|
||||||
|
--nodedc-priority-medium-rgb: 255 213 79;
|
||||||
|
--nodedc-priority-medium-mix-rgb: 80 126 255;
|
||||||
|
--nodedc-priority-high-rgb: 255 136 48;
|
||||||
|
--nodedc-priority-high-mix-rgb: 58 190 255;
|
||||||
|
--nodedc-priority-urgent-rgb: 255 68 82;
|
||||||
|
--nodedc-priority-urgent-mix-rgb: 48 214 188;
|
||||||
--nodedc-bottom-dock-height: 2.75rem;
|
--nodedc-bottom-dock-height: 2.75rem;
|
||||||
--nodedc-bottom-dock-offset: 2.75rem;
|
--nodedc-bottom-dock-offset: 2.75rem;
|
||||||
--nodedc-bottom-dock-visual-overlap: 0.625rem;
|
--nodedc-bottom-dock-visual-overlap: 0.625rem;
|
||||||
|
|
@ -215,6 +225,144 @@
|
||||||
animation: nodedcDropFillHighlight 1.6s ease-out;
|
animation: nodedcDropFillHighlight 1.6s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nodedc-work-item-card,
|
||||||
|
.nodedc-external-card {
|
||||||
|
--nodedc-priority-card-rgb: var(--nodedc-priority-none-rgb);
|
||||||
|
--nodedc-priority-card-mix-rgb: var(--nodedc-priority-none-mix-rgb);
|
||||||
|
--nodedc-priority-card-border-opacity: 0.08;
|
||||||
|
--nodedc-priority-card-fill-opacity: 0;
|
||||||
|
--nodedc-priority-card-glow-opacity: 0;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
isolation: isolate;
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px rgb(var(--nodedc-priority-card-rgb) / var(--nodedc-priority-card-border-opacity)),
|
||||||
|
0 12px 28px rgba(0, 0, 0, 0.2),
|
||||||
|
0 0 18px rgb(var(--nodedc-priority-card-rgb) / var(--nodedc-priority-card-glow-opacity)),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.035) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-work-item-card[data-priority="low"],
|
||||||
|
.nodedc-external-card[data-priority="low"] {
|
||||||
|
--nodedc-priority-card-rgb: var(--nodedc-priority-low-rgb);
|
||||||
|
--nodedc-priority-card-mix-rgb: var(--nodedc-priority-low-mix-rgb);
|
||||||
|
--nodedc-priority-card-border-opacity: 0.42;
|
||||||
|
--nodedc-priority-card-fill-opacity: 0.46;
|
||||||
|
--nodedc-priority-card-glow-opacity: 0.12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-work-item-card[data-priority="medium"],
|
||||||
|
.nodedc-external-card[data-priority="medium"] {
|
||||||
|
--nodedc-priority-card-rgb: var(--nodedc-priority-medium-rgb);
|
||||||
|
--nodedc-priority-card-mix-rgb: var(--nodedc-priority-medium-mix-rgb);
|
||||||
|
--nodedc-priority-card-border-opacity: 0.46;
|
||||||
|
--nodedc-priority-card-fill-opacity: 0.5;
|
||||||
|
--nodedc-priority-card-glow-opacity: 0.13;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-work-item-card[data-priority="high"],
|
||||||
|
.nodedc-external-card[data-priority="high"] {
|
||||||
|
--nodedc-priority-card-rgb: var(--nodedc-priority-high-rgb);
|
||||||
|
--nodedc-priority-card-mix-rgb: var(--nodedc-priority-high-mix-rgb);
|
||||||
|
--nodedc-priority-card-border-opacity: 0.5;
|
||||||
|
--nodedc-priority-card-fill-opacity: 0.54;
|
||||||
|
--nodedc-priority-card-glow-opacity: 0.15;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-work-item-card[data-priority="urgent"],
|
||||||
|
.nodedc-external-card[data-priority="urgent"] {
|
||||||
|
--nodedc-priority-card-rgb: var(--nodedc-priority-urgent-rgb);
|
||||||
|
--nodedc-priority-card-mix-rgb: var(--nodedc-priority-urgent-mix-rgb);
|
||||||
|
--nodedc-priority-card-border-opacity: 0.62;
|
||||||
|
--nodedc-priority-card-fill-opacity: 0.62;
|
||||||
|
--nodedc-priority-card-glow-opacity: 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-work-item-card[data-active="true"],
|
||||||
|
.nodedc-external-card[data-active="true"] {
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px rgb(var(--nodedc-priority-card-rgb) / var(--nodedc-priority-card-border-opacity)),
|
||||||
|
0 14px 32px rgba(0, 0, 0, 0.22),
|
||||||
|
0 0 20px rgb(var(--nodedc-priority-card-rgb) / var(--nodedc-priority-card-glow-opacity)),
|
||||||
|
inset 0 0 0 1px rgb(var(--nodedc-accent-rgb) / 0.18),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.06) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-work-item-card::before,
|
||||||
|
.nodedc-external-card::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
border-radius: inherit;
|
||||||
|
background:
|
||||||
|
radial-gradient(
|
||||||
|
120% 90% at 50% 118%,
|
||||||
|
rgb(var(--nodedc-priority-card-rgb) / 0.74) 0%,
|
||||||
|
rgb(var(--nodedc-priority-card-rgb) / 0.28) 30%,
|
||||||
|
transparent 64%
|
||||||
|
),
|
||||||
|
radial-gradient(
|
||||||
|
105% 78% at 50% -26%,
|
||||||
|
rgb(var(--nodedc-priority-card-mix-rgb) / 0.3) 0%,
|
||||||
|
transparent 62%
|
||||||
|
),
|
||||||
|
radial-gradient(
|
||||||
|
62% 70% at -18% 50%,
|
||||||
|
rgb(var(--nodedc-priority-card-rgb) / 0.24) 0%,
|
||||||
|
transparent 68%
|
||||||
|
),
|
||||||
|
radial-gradient(
|
||||||
|
62% 70% at 118% 50%,
|
||||||
|
rgb(var(--nodedc-priority-card-mix-rgb) / 0.22) 0%,
|
||||||
|
transparent 68%
|
||||||
|
);
|
||||||
|
opacity: var(--nodedc-priority-card-fill-opacity);
|
||||||
|
mask: radial-gradient(72% 58% at 50% 50%, transparent 0%, transparent 52%, rgba(0, 0, 0, 0.42) 68%, #000 100%);
|
||||||
|
-webkit-mask: radial-gradient(
|
||||||
|
72% 58% at 50% 50%,
|
||||||
|
transparent 0%,
|
||||||
|
transparent 52%,
|
||||||
|
rgba(0, 0, 0, 0.42) 68%,
|
||||||
|
#000 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-work-item-card::after,
|
||||||
|
.nodedc-external-card::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 0;
|
||||||
|
padding: 1px;
|
||||||
|
pointer-events: none;
|
||||||
|
border-radius: inherit;
|
||||||
|
background:
|
||||||
|
linear-gradient(
|
||||||
|
145deg,
|
||||||
|
rgb(var(--nodedc-priority-card-rgb) / 0.82) 0%,
|
||||||
|
rgb(var(--nodedc-priority-card-mix-rgb) / 0.44) 32%,
|
||||||
|
rgba(255, 255, 255, 0.18) 48%,
|
||||||
|
rgb(var(--nodedc-priority-card-rgb) / 0.34) 100%
|
||||||
|
);
|
||||||
|
opacity: var(--nodedc-priority-card-border-opacity);
|
||||||
|
mask:
|
||||||
|
linear-gradient(#000 0 0) content-box,
|
||||||
|
linear-gradient(#000 0 0);
|
||||||
|
mask-composite: exclude;
|
||||||
|
-webkit-mask:
|
||||||
|
linear-gradient(#000 0 0) content-box,
|
||||||
|
linear-gradient(#000 0 0);
|
||||||
|
-webkit-mask-composite: xor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-work-item-card > *,
|
||||||
|
.nodedc-external-card > * {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
/* Progress Bar Styles */
|
/* Progress Bar Styles */
|
||||||
:root {
|
:root {
|
||||||
--bprogress-color: var(--background-color-accent-primary);
|
--bprogress-color: var(--background-color-accent-primary);
|
||||||
|
|
@ -1384,6 +1532,25 @@
|
||||||
color: rgba(11, 17, 23, 0.72) !important;
|
color: rgba(11, 17, 23, 0.72) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nodedc-work-item-card,
|
||||||
|
.nodedc-external-card {
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px rgb(var(--nodedc-priority-card-rgb) / var(--nodedc-priority-card-border-opacity)),
|
||||||
|
0 12px 28px rgba(0, 0, 0, 0.2),
|
||||||
|
0 0 18px rgb(var(--nodedc-priority-card-rgb) / var(--nodedc-priority-card-glow-opacity)),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.035) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-work-item-card[data-active="true"],
|
||||||
|
.nodedc-external-card[data-active="true"] {
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px rgb(var(--nodedc-priority-card-rgb) / var(--nodedc-priority-card-border-opacity)),
|
||||||
|
0 14px 32px rgba(0, 0, 0, 0.22),
|
||||||
|
0 0 20px rgb(var(--nodedc-priority-card-rgb) / var(--nodedc-priority-card-glow-opacity)),
|
||||||
|
inset 0 0 0 1px rgb(var(--nodedc-accent-rgb) / 0.18),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.06) !important;
|
||||||
|
}
|
||||||
|
|
||||||
.nodedc-external-content-shell {
|
.nodedc-external-content-shell {
|
||||||
border: 0 !important;
|
border: 0 !important;
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
|
|
|
||||||
|
|
@ -1569,6 +1569,8 @@ export default {
|
||||||
change_email_modal: {
|
change_email_modal: {
|
||||||
title: "Change email",
|
title: "Change email",
|
||||||
description: "Enter a new email address to receive a verification link.",
|
description: "Enter a new email address to receive a verification link.",
|
||||||
|
direct_description:
|
||||||
|
"Enter a new email address. SMTP is not configured, so the address will be changed directly and you will need to sign in again.",
|
||||||
toasts: {
|
toasts: {
|
||||||
success_title: "Success!",
|
success_title: "Success!",
|
||||||
success_message: "Email updated successfully. Please sign in again.",
|
success_message: "Email updated successfully. Please sign in again.",
|
||||||
|
|
|
||||||
|
|
@ -1733,6 +1733,8 @@ export default {
|
||||||
change_email_modal: {
|
change_email_modal: {
|
||||||
title: "Изменить email",
|
title: "Изменить email",
|
||||||
description: "Введите новый адрес электронной почты, чтобы получить ссылку для подтверждения.",
|
description: "Введите новый адрес электронной почты, чтобы получить ссылку для подтверждения.",
|
||||||
|
direct_description:
|
||||||
|
"Введите новый адрес электронной почты. SMTP не настроен, поэтому адрес будет изменен напрямую, после сохранения потребуется войти снова.",
|
||||||
toasts: {
|
toasts: {
|
||||||
success_title: "Успех!",
|
success_title: "Успех!",
|
||||||
success_message: "Email успешно обновлён. Пожалуйста, войдите снова.",
|
success_message: "Email успешно обновлён. Пожалуйста, войдите снова.",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue