Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b2df70de4 | ||
|
|
43c1df2ee3 | ||
|
|
b01a8a3c5f | ||
|
|
6c1f707325 | ||
|
|
3eaf39439a | ||
|
|
e6d8594679 | ||
|
|
e9b307d372 | ||
|
|
b028d833c2 | ||
|
|
08b7370dbc | ||
|
|
2624a5c6e4 | ||
|
|
806cf4df1e | ||
|
|
d75efb3cfd | ||
|
|
ab9a6b4466 | ||
|
|
2f0ea09f15 | ||
|
|
841388a1d5 | ||
|
|
e7cf8fa925 | ||
|
|
280c3cec3d | ||
|
|
d874227de3 | ||
|
|
f75d40f85a | ||
|
|
e43d9b65ed | ||
|
|
6a09e51541 |
@@ -60,7 +60,7 @@ const InstanceEmailPage = observer(function InstanceEmailPage(_props: Route.Comp
|
||||
Plane can send useful emails to you and your users from your own instance without talking to the Internet.
|
||||
<div className="text-13 font-regular text-tertiary">
|
||||
Set it up below and please test your settings before you save them.
|
||||
<span className="text-danger-primary">Misconfigs can lead to email bounces and errors.</span>
|
||||
<span className="text-danger">Misconfigs can lead to email bounces and errors.</span>
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useState, useRef } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { HelpCircle, MoveLeft } from "lucide-react";
|
||||
import { ExternalLink, HelpCircle, MoveLeft } from "lucide-react";
|
||||
import { Transition } from "@headlessui/react";
|
||||
import { WEB_BASE_URL } from "@plane/constants";
|
||||
// plane internal packages
|
||||
import { DiscordIcon, GithubIcon, NewTabIcon, PageIcon } from "@plane/propel/icons";
|
||||
import { WEB_BASE_URL } from "@plane/constants";
|
||||
import { DiscordIcon, GithubIcon, PageIcon } from "@plane/propel/icons";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { cn } from "@plane/utils";
|
||||
// hooks
|
||||
@@ -56,7 +56,7 @@ export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection
|
||||
href={redirectionLink}
|
||||
className={`relative px-2 py-1 flex items-center gap-1 rounded-sm bg-layer-1 text-body-xs-medium text-secondary whitespace-nowrap`}
|
||||
>
|
||||
<NewTabIcon width={14} height={14} />
|
||||
<ExternalLink size={16} />
|
||||
{!isSidebarCollapsed && "Redirect to Plane"}
|
||||
</a>
|
||||
</Tooltip>
|
||||
|
||||
@@ -118,7 +118,7 @@ export function WorkspaceCreateForm() {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<span className="text-11 text-danger-primary">{errors?.name?.message}</span>
|
||||
<span className="text-11 text-red-500">{errors?.name?.message}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
@@ -153,11 +153,11 @@ export function WorkspaceCreateForm() {
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{slugError && <p className="text-13 text-danger-primary">This URL is taken. Try something else.</p>}
|
||||
{slugError && <p className="text-13 text-red-500">This URL is taken. Try something else.</p>}
|
||||
{invalidSlug && (
|
||||
<p className="text-13 text-danger-primary">{`URLs can contain only ( - ), ( _ ) and alphanumeric characters.`}</p>
|
||||
<p className="text-13 text-red-500">{`URLs can contain only ( - ), ( _ ) and alphanumeric characters.`}</p>
|
||||
)}
|
||||
{errors.slug && <span className="text-11 text-danger-primary">{errors.slug.message}</span>}
|
||||
{errors.slug && <span className="text-11 text-red-500">{errors.slug.message}</span>}
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-13 text-tertiary">How many people will use this workspace?</h4>
|
||||
@@ -187,7 +187,7 @@ export function WorkspaceCreateForm() {
|
||||
)}
|
||||
/>
|
||||
{errors.organization_size && (
|
||||
<span className="text-13 text-danger-primary">{errors.organization_size.message}</span>
|
||||
<span className="text-13 text-red-500">{errors.organization_size.message}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -129,7 +129,7 @@ export function InstanceSignInForm() {
|
||||
|
||||
<div className="w-full space-y-1">
|
||||
<label className="text-13 text-tertiary font-medium" htmlFor="email">
|
||||
Email <span className="text-danger-primary">*</span>
|
||||
Email <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Input
|
||||
className="w-full border border-subtle !bg-surface-1 placeholder:text-placeholder"
|
||||
@@ -147,7 +147,7 @@ export function InstanceSignInForm() {
|
||||
|
||||
<div className="w-full space-y-1">
|
||||
<label className="text-13 text-tertiary font-medium" htmlFor="password">
|
||||
Password <span className="text-danger-primary">*</span>
|
||||
Password <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<svg width="92" height="84" viewBox="0 0 92 84" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_3695_11896)">
|
||||
<path d="M83.0398 32.6876C74.2901 27.2397 62.0735 23.8553 48.7013 23.8553C21.7918 23.8553 0 37.3101 0 53.9016C0 69.0898 18.1598 81.554 41.685 83.7001V74.9504C25.8364 72.9693 13.95 64.3022 13.95 53.9016C13.95 42.0977 29.4684 32.44 48.7013 32.44C58.2765 32.44 66.9436 34.8338 73.217 38.7134L64.3022 44.2439H92.1197V27.0746L83.0398 32.6876Z" fill="#CCCCCC"/>
|
||||
<path d="M41.6846 8.99736V74.9504V83.7002L55.6346 74.9504V0L41.6846 8.99736Z" fill="#FF6200"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_3695_11896">
|
||||
<rect width="92" height="84" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 702 B |
@@ -0,0 +1,17 @@
|
||||
<svg width="700" height="650" viewBox="0 0 700 650" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_3262_5767)">
|
||||
<mask id="mask0_3262_5767" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="700" height="650">
|
||||
<path d="M700 0H0V650H700V0Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_3262_5767)">
|
||||
<path d="M337.682 0L360.832 20.5C377.982 35.7 395.182 50.85 412.132 66.25C521.982 166 614.982 278.25 684.982 407.45C688.582 414.05 691.832 420.85 695.082 427.6L699.982 437.75L694.582 440.6L690.532 434.85L680.032 419.9L672.682 409.2C621.732 335.25 570.682 261.2 500.582 201.95C479.373 183.995 455.969 168.807 430.932 156.75C380.232 132.5 335.132 142.2 296.432 182C259.632 219.85 240.532 266.85 223.282 314.65C221.032 320.8 218.682 326.9 216.332 333L212.232 343.75L203.632 341C208.632 323.6 213.232 306.1 217.832 288.55C228.332 248.8 238.832 209.05 253.432 170.75C268.932 129.95 288.532 90.6 308.082 51.25C316.532 34.2 324.982 17.15 333.082 0H337.682Z" fill="#C22E33"/>
|
||||
<path d="M372.382 491.1C291.082 529.6 94.3829 569.3 1.08287 559.1C-14.1671 478.8 135.482 102.5 208.982 45.5L204.232 56.4C202.115 61.531 199.813 66.5842 197.332 71.55L194.032 78C156.032 151.1 118.082 224.3 98.6329 304.5C91.6287 332.124 87.8038 360.458 87.2328 388.95C86.7328 455.95 128.432 501.55 198.082 504.4C231.582 505.75 265.432 502.25 299.232 498.7C313.932 497.2 328.582 495.65 343.232 494.5C348.632 494.1 353.932 493.45 360.832 492.55L372.382 491.15V491.1Z" fill="#C22E33"/>
|
||||
<path d="M141.233 639.05C118.983 640.75 96.733 642.45 74.583 644.45C279.433 663.95 476.083 630.6 670.083 562.25C606.833 450.75 521.583 362.7 422.483 286.15C423.783 291.05 426.683 294.6 429.533 298.1L431.933 301.1C440.433 312.4 449.333 323.5 458.283 334.6C478.733 360.05 499.183 385.5 514.583 413.5C553.483 484.5 532.383 545.9 456.183 578.3C406.083 599.65 351.333 614.2 297.183 622.9C245.683 631.1 193.433 635.05 141.233 639.05Z" fill="#C22E33"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_3262_5767">
|
||||
<rect width="700" height="650" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -10,22 +10,20 @@ export function Banner(props: TBanner) {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`rounded-md p-2 w-full border ${type === "error" ? "bg-danger-subtle border-danger-strong" : "bg-success-subtle border-success-strong"}`}
|
||||
className={`rounded-md p-2 w-full border ${type === "error" ? "bg-red-500/5 border-red-400" : "bg-green-500/5 border-green-400"}`}
|
||||
>
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="flex-shrink-0">
|
||||
{type === "error" ? (
|
||||
<span className="flex items-center justify-center h-6 w-6 rounded-full">
|
||||
<AlertCircle className="h-5 w-5 text-danger-primary" aria-hidden="true" />
|
||||
<AlertCircle className="h-5 w-5 text-red-600" aria-hidden="true" />
|
||||
</span>
|
||||
) : (
|
||||
<CheckCircle2 className="h-5 w-5 text-success-primary" aria-hidden="true" />
|
||||
<CheckCircle2 className="h-5 w-5 text-green-600" aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
<div className="ml-1">
|
||||
<p className={`text-13 font-medium ${type === "error" ? "text-danger-primary" : "text-success-primary"}`}>
|
||||
{message}
|
||||
</p>
|
||||
<p className={`text-13 font-medium ${type === "error" ? "text-red-600" : "text-green-600"}`}>{message}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
// ui
|
||||
import { Copy } from "lucide-react";
|
||||
import { Button } from "@plane/propel/button";
|
||||
import { CopyIcon } from "@plane/propel/icons";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
|
||||
type Props = {
|
||||
@@ -37,7 +37,7 @@ export function CopyField(props: Props) {
|
||||
}}
|
||||
>
|
||||
<p className="text-13 font-medium">{url}</p>
|
||||
<CopyIcon width={18} height={18} color="#B9B9B9" />
|
||||
<Copy size={18} color="#B9B9B9" />
|
||||
</Button>
|
||||
<div className="text-11 text-tertiary">{description}</div>
|
||||
</div>
|
||||
|
||||
@@ -157,7 +157,7 @@ export function InstanceSetupForm() {
|
||||
<div className="flex flex-col sm:flex-row items-center gap-4">
|
||||
<div className="w-full space-y-1">
|
||||
<label className="text-13 text-tertiary font-medium" htmlFor="first_name">
|
||||
First name <span className="text-danger-primary">*</span>
|
||||
First name <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Input
|
||||
className="w-full border border-subtle !bg-surface-1 placeholder:text-placeholder"
|
||||
@@ -174,7 +174,7 @@ export function InstanceSetupForm() {
|
||||
</div>
|
||||
<div className="w-full space-y-1">
|
||||
<label className="text-13 text-tertiary font-medium" htmlFor="last_name">
|
||||
Last name <span className="text-danger-primary">*</span>
|
||||
Last name <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Input
|
||||
className="w-full border border-subtle !bg-surface-1 placeholder:text-placeholder"
|
||||
@@ -192,7 +192,7 @@ export function InstanceSetupForm() {
|
||||
|
||||
<div className="w-full space-y-1">
|
||||
<label className="text-13 text-tertiary font-medium" htmlFor="email">
|
||||
Email <span className="text-danger-primary">*</span>
|
||||
Email <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Input
|
||||
className="w-full border border-subtle !bg-surface-1 placeholder:text-placeholder"
|
||||
@@ -207,13 +207,13 @@ export function InstanceSetupForm() {
|
||||
autoComplete="on"
|
||||
/>
|
||||
{errorData.type && errorData.type === EErrorCodes.INVALID_EMAIL && errorData.message && (
|
||||
<p className="px-1 text-11 text-danger-primary">{errorData.message}</p>
|
||||
<p className="px-1 text-11 text-red-500">{errorData.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="w-full space-y-1">
|
||||
<label className="text-13 text-tertiary font-medium" htmlFor="company_name">
|
||||
Company name <span className="text-danger-primary">*</span>
|
||||
Company name <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Input
|
||||
className="w-full border border-subtle !bg-surface-1 placeholder:text-placeholder"
|
||||
@@ -229,7 +229,7 @@ export function InstanceSetupForm() {
|
||||
|
||||
<div className="w-full space-y-1">
|
||||
<label className="text-13 text-tertiary font-medium" htmlFor="password">
|
||||
Set a password <span className="text-danger-primary">*</span>
|
||||
Set a password <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
@@ -267,14 +267,14 @@ export function InstanceSetupForm() {
|
||||
)}
|
||||
</div>
|
||||
{errorData.type && errorData.type === EErrorCodes.INVALID_PASSWORD && errorData.message && (
|
||||
<p className="px-1 text-11 text-danger-primary">{errorData.message}</p>
|
||||
<p className="px-1 text-11 text-red-500">{errorData.message}</p>
|
||||
)}
|
||||
<PasswordStrengthIndicator password={formData.password} isFocused={isPasswordInputFocused} />
|
||||
</div>
|
||||
|
||||
<div className="w-full space-y-1">
|
||||
<label className="text-13 text-tertiary font-medium" htmlFor="confirm_password">
|
||||
Confirm password <span className="text-danger-primary">*</span>
|
||||
Confirm password <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
@@ -311,9 +311,7 @@ export function InstanceSetupForm() {
|
||||
</div>
|
||||
{!!formData.confirm_password &&
|
||||
formData.password !== formData.confirm_password &&
|
||||
renderPasswordMatchError && (
|
||||
<span className="text-13 text-danger-primary">Passwords don{"'"}t match</span>
|
||||
)}
|
||||
renderPasswordMatchError && <span className="text-13 text-red-500">Passwords don{"'"}t match</span>}
|
||||
</div>
|
||||
|
||||
<div className="relative flex gap-2">
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
import { ExternalLink } from "lucide-react";
|
||||
// plane internal packages
|
||||
import { WEB_BASE_URL } from "@plane/constants";
|
||||
import { NewTabIcon } from "@plane/propel/icons";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { getFileURL } from "@plane/utils";
|
||||
// hooks
|
||||
@@ -76,7 +75,7 @@ export const WorkspaceListItem = observer(function WorkspaceListItem({ workspace
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-shrink-0">
|
||||
<NewTabIcon width={14} height={16} className="text-placeholder group-hover:text-secondary" />
|
||||
<ExternalLink size={16} className="text-placeholder group-hover:text-secondary" />
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Image, BrainCog, Cog, Mail } from "lucide-react";
|
||||
import { Image, BrainCog, Cog, Lock, Mail } from "lucide-react";
|
||||
// plane imports
|
||||
import { LockIcon, WorkspaceIcon } from "@plane/propel/icons";
|
||||
import { WorkspaceIcon } from "@plane/propel/icons";
|
||||
// types
|
||||
import type { TSidebarMenuItem } from "./types";
|
||||
|
||||
@@ -26,7 +26,7 @@ export const coreSidebarMenuLinks: Record<TCoreSidebarMenuKey, TSidebarMenuItem>
|
||||
href: `/workspace/`,
|
||||
},
|
||||
authentication: {
|
||||
Icon: LockIcon,
|
||||
Icon: Lock,
|
||||
name: "Authentication",
|
||||
description: "Configure authentication modes.",
|
||||
href: `/authentication/`,
|
||||
|
||||
@@ -20,12 +20,6 @@ http {
|
||||
server {
|
||||
listen 3000;
|
||||
|
||||
# Security headers
|
||||
add_header X-Frame-Options "DENY" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
"react-dom": "catalog:",
|
||||
"react-hook-form": "7.51.5",
|
||||
"react-router": "catalog:",
|
||||
"react-router-dom": "catalog:",
|
||||
"serve": "14.2.5",
|
||||
"swr": "catalog:",
|
||||
"uuid": "catalog:"
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
# Django imports
|
||||
from django.utils import timezone
|
||||
from django.db.models import Q
|
||||
from django.urls import resolve
|
||||
|
||||
# Third party imports
|
||||
from rest_framework import authentication
|
||||
from rest_framework.exceptions import AuthenticationFailed
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import APIToken
|
||||
from plane.db.models import APIToken, Workspace
|
||||
|
||||
|
||||
class APIKeyAuthentication(authentication.BaseAuthentication):
|
||||
@@ -22,13 +23,20 @@ class APIKeyAuthentication(authentication.BaseAuthentication):
|
||||
def get_api_token(self, request):
|
||||
return request.headers.get(self.auth_header_name)
|
||||
|
||||
def validate_api_token(self, token):
|
||||
def validate_api_token(self, token, workspace_slug):
|
||||
try:
|
||||
api_token = APIToken.objects.get(
|
||||
Q(Q(expired_at__gt=timezone.now()) | Q(expired_at__isnull=True)),
|
||||
token=token,
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
if workspace_slug:
|
||||
workspace = Workspace.objects.get(slug=workspace_slug)
|
||||
|
||||
if api_token.workspace_id != workspace.id:
|
||||
raise AuthenticationFailed("Given API token is not valid")
|
||||
|
||||
except APIToken.DoesNotExist:
|
||||
raise AuthenticationFailed("Given API token is not valid")
|
||||
|
||||
@@ -38,10 +46,12 @@ class APIKeyAuthentication(authentication.BaseAuthentication):
|
||||
return (api_token.user, api_token.token)
|
||||
|
||||
def authenticate(self, request):
|
||||
workspace_slug = resolve(request.path_info).kwargs.get("slug")
|
||||
|
||||
token = self.get_api_token(request=request)
|
||||
if not token:
|
||||
return None
|
||||
|
||||
# Validate the API token
|
||||
user, token = self.validate_api_token(token)
|
||||
user, token = self.validate_api_token(token, workspace_slug)
|
||||
return user, token
|
||||
|
||||
@@ -3,6 +3,7 @@ import os
|
||||
|
||||
# Third party imports
|
||||
from rest_framework.throttling import SimpleRateThrottle
|
||||
from plane.db.models import APIToken
|
||||
|
||||
|
||||
class ApiKeyRateThrottle(SimpleRateThrottle):
|
||||
@@ -85,3 +86,41 @@ class ServiceTokenRateThrottle(SimpleRateThrottle):
|
||||
request.META["X-RateLimit-Reset"] = reset_time
|
||||
|
||||
return allowed
|
||||
|
||||
|
||||
class WorkspaceTokenRateThrottle(SimpleRateThrottle):
|
||||
scope = "workspace_token"
|
||||
rate = "60/minute"
|
||||
|
||||
def get_cache_key(self, request, view):
|
||||
api_key = request.headers.get("X-Api-Key")
|
||||
if not api_key:
|
||||
return None
|
||||
|
||||
return f"{self.scope}:{api_key}"
|
||||
|
||||
def allow_request(self, request, view):
|
||||
api_key = request.headers.get("X-Api-Key")
|
||||
|
||||
if api_key:
|
||||
token = APIToken.objects.filter(token=api_key).only("allowed_rate_limit").first()
|
||||
if token and token.allowed_rate_limit:
|
||||
self.rate = token.allowed_rate_limit
|
||||
|
||||
self.num_requests, self.duration = self.parse_rate(self.rate)
|
||||
|
||||
allowed = super().allow_request(request, view)
|
||||
|
||||
if allowed:
|
||||
now = self.timer()
|
||||
history = self.cache.get(self.key, [])
|
||||
|
||||
while history and history[-1] <= now - self.duration:
|
||||
history.pop()
|
||||
|
||||
available = self.num_requests - len(history)
|
||||
|
||||
request.META["X-RateLimit-Remaining"] = max(0, available)
|
||||
request.META["X-RateLimit-Reset"] = int(now + self.duration)
|
||||
|
||||
return allowed
|
||||
|
||||
@@ -20,7 +20,7 @@ from rest_framework.generics import GenericAPIView
|
||||
# Module imports
|
||||
from plane.db.models.api import APIToken
|
||||
from plane.api.middleware.api_authentication import APIKeyAuthentication
|
||||
from plane.api.rate_limit import ApiKeyRateThrottle, ServiceTokenRateThrottle
|
||||
from plane.api.rate_limit import ApiKeyRateThrottle, ServiceTokenRateThrottle, WorkspaceTokenRateThrottle
|
||||
from plane.utils.exception_logger import log_exception
|
||||
from plane.utils.paginator import BasePaginator
|
||||
from plane.utils.core.mixins import ReadReplicaControlMixin
|
||||
@@ -60,12 +60,20 @@ class BaseAPIView(TimezoneMixin, GenericAPIView, ReadReplicaControlMixin, BasePa
|
||||
api_key = self.request.headers.get("X-Api-Key")
|
||||
|
||||
if api_key:
|
||||
service_token = APIToken.objects.filter(token=api_key, is_service=True).first()
|
||||
api_token = APIToken.objects.filter(token=api_key)
|
||||
|
||||
service_token = api_token.filter(is_service=True).first()
|
||||
|
||||
workspace_token = api_token.filter(workspace_id__isnull=False).first()
|
||||
|
||||
if service_token:
|
||||
throttle_classes.append(ServiceTokenRateThrottle())
|
||||
return throttle_classes
|
||||
|
||||
if workspace_token:
|
||||
throttle_classes.append(WorkspaceTokenRateThrottle())
|
||||
return throttle_classes
|
||||
|
||||
throttle_classes.append(ApiKeyRateThrottle())
|
||||
|
||||
return throttle_classes
|
||||
|
||||
@@ -18,7 +18,7 @@ from drf_spectacular.utils import OpenApiResponse, OpenApiRequest
|
||||
from plane.db.models import (
|
||||
Cycle,
|
||||
Intake,
|
||||
ProjectUserProperty,
|
||||
IssueUserProperty,
|
||||
Module,
|
||||
Project,
|
||||
DeployBoard,
|
||||
@@ -218,6 +218,8 @@ class ProjectListCreateAPIEndpoint(BaseAPIView):
|
||||
|
||||
# Add the user as Administrator to the project
|
||||
_ = ProjectMember.objects.create(project_id=serializer.instance.id, member=request.user, role=20)
|
||||
# Also create the issue property for the user
|
||||
_ = IssueUserProperty.objects.create(project_id=serializer.instance.id, user=request.user)
|
||||
|
||||
if serializer.instance.project_lead is not None and str(serializer.instance.project_lead) != str(
|
||||
request.user.id
|
||||
@@ -227,6 +229,11 @@ class ProjectListCreateAPIEndpoint(BaseAPIView):
|
||||
member_id=serializer.instance.project_lead,
|
||||
role=20,
|
||||
)
|
||||
# Also create the issue property for the user
|
||||
IssueUserProperty.objects.create(
|
||||
project_id=serializer.instance.id,
|
||||
user_id=serializer.instance.project_lead,
|
||||
)
|
||||
|
||||
State.objects.bulk_create(
|
||||
[
|
||||
|
||||
@@ -52,7 +52,7 @@ from .issue import (
|
||||
IssueCreateSerializer,
|
||||
IssueActivitySerializer,
|
||||
IssueCommentSerializer,
|
||||
ProjectUserPropertySerializer,
|
||||
IssueUserPropertySerializer,
|
||||
IssueAssigneeSerializer,
|
||||
LabelSerializer,
|
||||
IssueSerializer,
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
# Django import
|
||||
from django.utils import timezone
|
||||
|
||||
# Third party import
|
||||
from rest_framework import serializers
|
||||
|
||||
# Module import
|
||||
from .base import BaseSerializer
|
||||
from plane.db.models import APIToken, APIActivityLog
|
||||
from rest_framework import serializers
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
class APITokenSerializer(BaseSerializer):
|
||||
|
||||
@@ -18,7 +18,7 @@ from plane.db.models import (
|
||||
Issue,
|
||||
IssueActivity,
|
||||
IssueComment,
|
||||
ProjectUserProperty,
|
||||
IssueUserProperty,
|
||||
IssueAssignee,
|
||||
IssueSubscriber,
|
||||
IssueLabel,
|
||||
@@ -346,9 +346,9 @@ class IssueActivitySerializer(BaseSerializer):
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class ProjectUserPropertySerializer(BaseSerializer):
|
||||
class IssueUserPropertySerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = ProjectUserProperty
|
||||
model = IssueUserProperty
|
||||
fields = "__all__"
|
||||
read_only_fields = ["user", "workspace", "project"]
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from django.urls import path
|
||||
from plane.app.views import ApiTokenEndpoint, ServiceApiTokenEndpoint
|
||||
from plane.app.views import ApiTokenEndpoint, ServiceApiTokenEndpoint, WorkspaceAPITokenEndpoint
|
||||
|
||||
urlpatterns = [
|
||||
# API Tokens
|
||||
@@ -18,5 +18,15 @@ urlpatterns = [
|
||||
ServiceApiTokenEndpoint.as_view(),
|
||||
name="service-api-tokens",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/api-tokens/",
|
||||
WorkspaceAPITokenEndpoint.as_view(),
|
||||
name="workspace-api-tokens",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/api-tokens/<uuid:pk>/",
|
||||
WorkspaceAPITokenEndpoint.as_view(),
|
||||
name="workspace-api-tokens-details",
|
||||
),
|
||||
## End API Tokens
|
||||
]
|
||||
|
||||
@@ -14,7 +14,7 @@ from plane.app.views import (
|
||||
IssueReactionViewSet,
|
||||
IssueRelationViewSet,
|
||||
IssueSubscriberViewSet,
|
||||
ProjectUserDisplayPropertyEndpoint,
|
||||
IssueUserDisplayPropertyEndpoint,
|
||||
IssueViewSet,
|
||||
LabelViewSet,
|
||||
BulkArchiveIssuesEndpoint,
|
||||
@@ -208,13 +208,13 @@ urlpatterns = [
|
||||
name="project-issue-comment-reactions",
|
||||
),
|
||||
## End Comment Reactions
|
||||
## ProjectUserProperty
|
||||
## IssueUserProperty
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/user-properties/",
|
||||
ProjectUserDisplayPropertyEndpoint.as_view(),
|
||||
IssueUserDisplayPropertyEndpoint.as_view(),
|
||||
name="project-issue-display-properties",
|
||||
),
|
||||
## ProjectUserProperty End
|
||||
## IssueUserProperty End
|
||||
## Issue Archives
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-issues/",
|
||||
|
||||
@@ -114,7 +114,7 @@ from .asset.v2 import (
|
||||
from .issue.base import (
|
||||
IssueListEndpoint,
|
||||
IssueViewSet,
|
||||
ProjectUserDisplayPropertyEndpoint,
|
||||
IssueUserDisplayPropertyEndpoint,
|
||||
BulkDeleteIssuesEndpoint,
|
||||
DeletedIssuesListViewSet,
|
||||
IssuePaginatedViewSet,
|
||||
@@ -161,7 +161,7 @@ from .module.issue import ModuleIssueViewSet
|
||||
|
||||
from .module.archive import ModuleArchiveUnarchiveEndpoint
|
||||
|
||||
from .api import ApiTokenEndpoint, ServiceApiTokenEndpoint
|
||||
from .api import ApiTokenEndpoint, ServiceApiTokenEndpoint, WorkspaceAPITokenEndpoint
|
||||
|
||||
from .page.base import (
|
||||
PageViewSet,
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
from .base import ApiTokenEndpoint
|
||||
from .service import ServiceApiTokenEndpoint
|
||||
from .workspace import WorkspaceAPITokenEndpoint
|
||||
@@ -8,10 +8,9 @@ from rest_framework.request import Request
|
||||
from rest_framework import status
|
||||
|
||||
# Module import
|
||||
from .base import BaseAPIView
|
||||
from plane.db.models import APIToken, Workspace
|
||||
from plane.app.views.base import BaseAPIView
|
||||
from plane.db.models import APIToken
|
||||
from plane.app.serializers import APITokenSerializer, APITokenReadSerializer
|
||||
from plane.app.permissions import WorkspaceEntityPermission
|
||||
|
||||
|
||||
class ApiTokenEndpoint(BaseAPIView):
|
||||
@@ -37,11 +36,11 @@ class ApiTokenEndpoint(BaseAPIView):
|
||||
|
||||
def get(self, request: Request, pk: Optional[str] = None) -> Response:
|
||||
if pk is None:
|
||||
api_tokens = APIToken.objects.filter(user=request.user, is_service=False)
|
||||
api_tokens = APIToken.objects.filter(user=request.user, is_service=False, workspace_id__isnull=True)
|
||||
serializer = APITokenReadSerializer(api_tokens, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
else:
|
||||
api_tokens = APIToken.objects.get(user=request.user, pk=pk)
|
||||
api_tokens = APIToken.objects.get(user=request.user, pk=pk, workspace_id__isnull=True)
|
||||
serializer = APITokenReadSerializer(api_tokens)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -57,28 +56,3 @@ class ApiTokenEndpoint(BaseAPIView):
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class ServiceApiTokenEndpoint(BaseAPIView):
|
||||
permission_classes = [WorkspaceEntityPermission]
|
||||
|
||||
def post(self, request: Request, slug: str) -> Response:
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
|
||||
api_token = APIToken.objects.filter(workspace=workspace, is_service=True).first()
|
||||
|
||||
if api_token:
|
||||
return Response({"token": str(api_token.token)}, status=status.HTTP_200_OK)
|
||||
else:
|
||||
# Check the user type
|
||||
user_type = 1 if request.user.is_bot else 0
|
||||
|
||||
api_token = APIToken.objects.create(
|
||||
label=str(uuid4().hex),
|
||||
description="Service Token",
|
||||
user=request.user,
|
||||
workspace=workspace,
|
||||
user_type=user_type,
|
||||
is_service=True,
|
||||
)
|
||||
return Response({"token": str(api_token.token)}, status=status.HTTP_201_CREATED)
|
||||
@@ -0,0 +1,37 @@
|
||||
# Python import
|
||||
from uuid import uuid4
|
||||
|
||||
# Third party
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.request import Request
|
||||
from rest_framework import status
|
||||
|
||||
# Module import
|
||||
from .base import BaseAPIView
|
||||
from plane.db.models import APIToken, Workspace
|
||||
from plane.app.permissions import WorkspaceEntityPermission
|
||||
|
||||
|
||||
class ServiceApiTokenEndpoint(BaseAPIView):
|
||||
permission_classes = [WorkspaceEntityPermission]
|
||||
|
||||
def post(self, request: Request, slug: str) -> Response:
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
|
||||
api_token = APIToken.objects.filter(workspace=workspace, is_service=True).first()
|
||||
|
||||
if api_token:
|
||||
return Response({"token": str(api_token.token)}, status=status.HTTP_200_OK)
|
||||
else:
|
||||
# Check the user type
|
||||
user_type = 1 if request.user.is_bot else 0
|
||||
|
||||
api_token = APIToken.objects.create(
|
||||
label=str(uuid4().hex),
|
||||
description="Service Token",
|
||||
user=request.user,
|
||||
workspace=workspace,
|
||||
user_type=user_type,
|
||||
is_service=True,
|
||||
)
|
||||
return Response({"token": str(api_token.token)}, status=status.HTTP_201_CREATED)
|
||||
@@ -0,0 +1,59 @@
|
||||
# Python import
|
||||
from typing import Optional
|
||||
from uuid import uuid4
|
||||
|
||||
|
||||
# Third party
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.request import Request
|
||||
from rest_framework import status
|
||||
|
||||
# Module import
|
||||
from plane.app.views import BaseAPIView
|
||||
from plane.db.models import APIToken, Workspace
|
||||
from plane.app.serializers import APITokenSerializer, APITokenReadSerializer
|
||||
from plane.app.permissions import WorkSpaceAdminPermission
|
||||
|
||||
|
||||
class WorkspaceAPITokenEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
WorkSpaceAdminPermission,
|
||||
]
|
||||
|
||||
def post(self, request: Request, slug: str) -> Response:
|
||||
label = request.data.get("label", str(uuid4().hex))
|
||||
description = request.data.get("description", "")
|
||||
expired_at = request.data.get("expired_at", None)
|
||||
|
||||
# Check the user type
|
||||
user_type = 1 if request.user.is_bot else 0
|
||||
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
|
||||
api_token = APIToken.objects.create(
|
||||
label=label,
|
||||
description=description,
|
||||
user=request.user,
|
||||
user_type=user_type,
|
||||
expired_at=expired_at,
|
||||
workspace=workspace,
|
||||
)
|
||||
|
||||
serializer = APITokenSerializer(api_token)
|
||||
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
def get(self, request: Request, slug: str, pk: Optional[str] = None) -> Response:
|
||||
if pk is None:
|
||||
api_tokens = APIToken.objects.filter(workspace__slug=slug, is_service=False, user=request.user)
|
||||
serializer = APITokenReadSerializer(api_tokens, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
else:
|
||||
api_tokens = APIToken.objects.get(workspace__slug=slug, pk=pk, user=request.user)
|
||||
serializer = APITokenReadSerializer(api_tokens)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
def delete(self, request: Request, slug: str, pk: str) -> Response:
|
||||
api_token = APIToken.objects.get(workspace__slug=slug, pk=pk, is_service=False, user=request.user)
|
||||
api_token.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
@@ -34,7 +34,7 @@ from plane.app.serializers import (
|
||||
IssueDetailSerializer,
|
||||
IssueListDetailSerializer,
|
||||
IssueSerializer,
|
||||
ProjectUserPropertySerializer,
|
||||
IssueUserPropertySerializer,
|
||||
)
|
||||
from plane.bgtasks.issue_activities_task import issue_activity
|
||||
from plane.bgtasks.issue_description_version_task import issue_description_version_task
|
||||
@@ -51,7 +51,7 @@ from plane.db.models import (
|
||||
IssueReaction,
|
||||
IssueRelation,
|
||||
IssueSubscriber,
|
||||
ProjectUserProperty,
|
||||
IssueUserProperty,
|
||||
ModuleIssue,
|
||||
Project,
|
||||
ProjectMember,
|
||||
@@ -723,33 +723,23 @@ class IssueViewSet(BaseViewSet):
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class ProjectUserDisplayPropertyEndpoint(BaseAPIView):
|
||||
class IssueUserDisplayPropertyEndpoint(BaseAPIView):
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def patch(self, request, slug, project_id):
|
||||
try:
|
||||
issue_property = ProjectUserProperty.objects.get(
|
||||
user=request.user,
|
||||
project_id=project_id
|
||||
)
|
||||
except ProjectUserProperty.DoesNotExist:
|
||||
issue_property = ProjectUserProperty.objects.create(
|
||||
user=request.user,
|
||||
project_id=project_id
|
||||
)
|
||||
issue_property = IssueUserProperty.objects.get(user=request.user, project_id=project_id)
|
||||
|
||||
serializer = ProjectUserPropertySerializer(
|
||||
issue_property,
|
||||
data=request.data,
|
||||
partial=True
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
issue_property.rich_filters = request.data.get("rich_filters", issue_property.rich_filters)
|
||||
issue_property.filters = request.data.get("filters", issue_property.filters)
|
||||
issue_property.display_filters = request.data.get("display_filters", issue_property.display_filters)
|
||||
issue_property.display_properties = request.data.get("display_properties", issue_property.display_properties)
|
||||
issue_property.save()
|
||||
serializer = IssueUserPropertySerializer(issue_property)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def get(self, request, slug, project_id):
|
||||
issue_property, _ = ProjectUserProperty.objects.get_or_create(user=request.user, project_id=project_id)
|
||||
serializer = ProjectUserPropertySerializer(issue_property)
|
||||
issue_property, _ = IssueUserProperty.objects.get_or_create(user=request.user, project_id=project_id)
|
||||
serializer = IssueUserPropertySerializer(issue_property)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
|
||||
@@ -24,15 +24,14 @@ from plane.bgtasks.webhook_task import model_activity, webhook_activity
|
||||
from plane.db.models import (
|
||||
UserFavorite,
|
||||
DeployBoard,
|
||||
ProjectUserProperty,
|
||||
Intake,
|
||||
IssueUserProperty,
|
||||
Project,
|
||||
ProjectIdentifier,
|
||||
ProjectMember,
|
||||
ProjectNetwork,
|
||||
State,
|
||||
DEFAULT_STATES,
|
||||
UserFavorite,
|
||||
Workspace,
|
||||
WorkspaceMember,
|
||||
)
|
||||
@@ -251,6 +250,8 @@ class ProjectViewSet(BaseViewSet):
|
||||
member=request.user,
|
||||
role=ROLE.ADMIN.value,
|
||||
)
|
||||
# Also create the issue property for the user
|
||||
_ = IssueUserProperty.objects.create(project_id=serializer.data["id"], user=request.user)
|
||||
|
||||
if serializer.data["project_lead"] is not None and str(serializer.data["project_lead"]) != str(
|
||||
request.user.id
|
||||
@@ -260,6 +261,11 @@ class ProjectViewSet(BaseViewSet):
|
||||
member_id=serializer.data["project_lead"],
|
||||
role=ROLE.ADMIN.value,
|
||||
)
|
||||
# Also create the issue property for the user
|
||||
IssueUserProperty.objects.create(
|
||||
project_id=serializer.data["id"],
|
||||
user_id=serializer.data["project_lead"],
|
||||
)
|
||||
|
||||
State.objects.bulk_create(
|
||||
[
|
||||
|
||||
@@ -24,7 +24,7 @@ from plane.db.models import (
|
||||
User,
|
||||
WorkspaceMember,
|
||||
Project,
|
||||
ProjectUserProperty,
|
||||
IssueUserProperty,
|
||||
)
|
||||
from plane.db.models.project import ProjectNetwork
|
||||
from plane.utils.host import base_host
|
||||
@@ -160,9 +160,9 @@ class UserProjectInvitationsViewset(BaseViewSet):
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
|
||||
ProjectUserProperty.objects.bulk_create(
|
||||
IssueUserProperty.objects.bulk_create(
|
||||
[
|
||||
ProjectUserProperty(
|
||||
IssueUserProperty(
|
||||
project_id=project_id,
|
||||
user=request.user,
|
||||
workspace=workspace,
|
||||
@@ -220,7 +220,7 @@ class ProjectJoinEndpoint(BaseAPIView):
|
||||
if project_member is None:
|
||||
# Create a Project Member
|
||||
_ = ProjectMember.objects.create(
|
||||
project_id=project_id,
|
||||
workspace_id=project_invite.workspace_id,
|
||||
member=user,
|
||||
role=project_invite.role,
|
||||
)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# Third Party imports
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from django.db.models import Min
|
||||
|
||||
# Module imports
|
||||
from .base import BaseViewSet, BaseAPIView
|
||||
@@ -14,7 +13,7 @@ from plane.app.serializers import (
|
||||
|
||||
from plane.app.permissions import WorkspaceUserPermission
|
||||
|
||||
from plane.db.models import Project, ProjectMember, ProjectUserProperty, WorkspaceMember
|
||||
from plane.db.models import Project, ProjectMember, IssueUserProperty, WorkspaceMember
|
||||
from plane.bgtasks.project_add_user_email_task import project_add_user_email
|
||||
from plane.utils.host import base_host
|
||||
from plane.app.permissions.base import allow_permission, ROLE
|
||||
@@ -90,23 +89,24 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
# Update the roles of the existing members
|
||||
ProjectMember.objects.bulk_update(bulk_project_members, ["is_active", "role"], batch_size=100)
|
||||
|
||||
# Get the minimum sort_order for each member in the workspace
|
||||
member_sort_orders = (
|
||||
ProjectUserProperty.objects.filter(
|
||||
# Get the list of project members of the requested workspace with the given slug
|
||||
project_members = (
|
||||
ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
user_id__in=[member.get("member_id") for member in members],
|
||||
member_id__in=[member.get("member_id") for member in members],
|
||||
)
|
||||
.values("user_id")
|
||||
.annotate(min_sort_order=Min("sort_order"))
|
||||
.values("member_id", "sort_order")
|
||||
.order_by("sort_order")
|
||||
)
|
||||
# Convert to dictionary for easy lookup: {user_id: min_sort_order}
|
||||
sort_order_map = {str(item["user_id"]): item["min_sort_order"] for item in member_sort_orders}
|
||||
|
||||
# Loop through requested members
|
||||
for member in members:
|
||||
member_id = str(member.get("member_id"))
|
||||
# Get the minimum sort_order for this member, or use default
|
||||
min_sort_order = sort_order_map.get(member_id)
|
||||
# Get the sort orders of the member
|
||||
sort_order = [
|
||||
project_member.get("sort_order")
|
||||
for project_member in project_members
|
||||
if str(project_member.get("member_id")) == str(member.get("member_id"))
|
||||
]
|
||||
# Create a new project member
|
||||
bulk_project_members.append(
|
||||
ProjectMember(
|
||||
@@ -114,22 +114,22 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
role=member.get("role", 5),
|
||||
project_id=project_id,
|
||||
workspace_id=project.workspace_id,
|
||||
sort_order=(sort_order[0] - 10000 if len(sort_order) else 65535),
|
||||
)
|
||||
)
|
||||
# Create a new issue property
|
||||
bulk_issue_props.append(
|
||||
ProjectUserProperty(
|
||||
IssueUserProperty(
|
||||
user_id=member.get("member_id"),
|
||||
project_id=project_id,
|
||||
workspace_id=project.workspace_id,
|
||||
sort_order=(min_sort_order - 10000 if min_sort_order is not None else 65535),
|
||||
)
|
||||
)
|
||||
|
||||
# Bulk create the project members and issue properties
|
||||
project_members = ProjectMember.objects.bulk_create(bulk_project_members, batch_size=10, ignore_conflicts=True)
|
||||
|
||||
_ = ProjectUserProperty.objects.bulk_create(bulk_issue_props, batch_size=10, ignore_conflicts=True)
|
||||
_ = IssueUserProperty.objects.bulk_create(bulk_issue_props, batch_size=10, ignore_conflicts=True)
|
||||
|
||||
project_members = ProjectMember.objects.filter(
|
||||
project_id=project_id,
|
||||
|
||||
@@ -77,12 +77,14 @@ class WorkSpaceViewSet(BaseViewSet):
|
||||
|
||||
def create(self, request):
|
||||
try:
|
||||
(DISABLE_WORKSPACE_CREATION,) = get_configuration_value([
|
||||
{
|
||||
"key": "DISABLE_WORKSPACE_CREATION",
|
||||
"default": os.environ.get("DISABLE_WORKSPACE_CREATION", "0"),
|
||||
}
|
||||
])
|
||||
(DISABLE_WORKSPACE_CREATION,) = get_configuration_value(
|
||||
[
|
||||
{
|
||||
"key": "DISABLE_WORKSPACE_CREATION",
|
||||
"default": os.environ.get("DISABLE_WORKSPACE_CREATION", "0"),
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
if DISABLE_WORKSPACE_CREATION == "1":
|
||||
return Response(
|
||||
|
||||
@@ -26,6 +26,7 @@ from plane.bgtasks.workspace_invitation_task import workspace_invitation
|
||||
from plane.db.models import User, Workspace, WorkspaceMember, WorkspaceMemberInvite
|
||||
from plane.utils.cache import invalidate_cache, invalidate_cache_directly
|
||||
from plane.utils.host import base_host
|
||||
from plane.utils.ip_address import get_client_ip
|
||||
from plane.utils.analytics_events import USER_JOINED_WORKSPACE, USER_INVITED_TO_WORKSPACE
|
||||
from .. import BaseViewSet
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Python imports
|
||||
import logging
|
||||
|
||||
|
||||
# Third party imports
|
||||
from celery import shared_task
|
||||
import requests
|
||||
@@ -19,34 +20,6 @@ logger = logging.getLogger("plane.worker")
|
||||
DEFAULT_FAVICON = "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLWxpbmstaWNvbiBsdWNpZGUtbGluayI+PHBhdGggZD0iTTEwIDEzYTUgNSAwIDAgMCA3LjU0LjU0bDMtM2E1IDUgMCAwIDAtNy4wNy03LjA3bC0xLjcyIDEuNzEiLz48cGF0aCBkPSJNMTQgMTFhNSA1IDAgMCAwLTcuNTQtLjU0bC0zIDNhNSA1IDAgMCAwIDcuMDcgNy4wN2wxLjcxLTEuNzEiLz48L3N2Zz4=" # noqa: E501
|
||||
|
||||
|
||||
def validate_url_ip(url: str) -> None:
|
||||
"""
|
||||
Validate that a URL doesn't point to a private/internal IP address.
|
||||
Only checks if the hostname is a direct IP address.
|
||||
|
||||
Args:
|
||||
url: The URL to validate
|
||||
|
||||
Raises:
|
||||
ValueError: If the URL points to a private/internal IP
|
||||
"""
|
||||
parsed = urlparse(url)
|
||||
hostname = parsed.hostname
|
||||
|
||||
if not hostname:
|
||||
return
|
||||
|
||||
try:
|
||||
ip = ipaddress.ip_address(hostname)
|
||||
except ValueError:
|
||||
# Not an IP address (it's a domain name), nothing to check here
|
||||
return
|
||||
|
||||
# It IS an IP address - check if it's private/internal
|
||||
if ip.is_private or ip.is_loopback or ip.is_reserved:
|
||||
raise ValueError("Access to private/internal networks is not allowed")
|
||||
|
||||
|
||||
def crawl_work_item_link_title_and_favicon(url: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Crawls a URL to extract the title and favicon.
|
||||
@@ -58,6 +31,17 @@ def crawl_work_item_link_title_and_favicon(url: str) -> Dict[str, Any]:
|
||||
str: JSON string containing title and base64-encoded favicon
|
||||
"""
|
||||
try:
|
||||
# Prevent access to private IP ranges
|
||||
parsed = urlparse(url)
|
||||
|
||||
try:
|
||||
ip = ipaddress.ip_address(parsed.hostname)
|
||||
if ip.is_private or ip.is_loopback or ip.is_reserved:
|
||||
raise ValueError("Access to private/internal networks is not allowed")
|
||||
except ValueError:
|
||||
# Not an IP address, continue with domain validation
|
||||
pass
|
||||
|
||||
# Set up headers to mimic a real browser
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" # noqa: E501
|
||||
@@ -65,16 +49,9 @@ def crawl_work_item_link_title_and_favicon(url: str) -> Dict[str, Any]:
|
||||
|
||||
soup = None
|
||||
title = None
|
||||
final_url = url
|
||||
|
||||
validate_url_ip(final_url)
|
||||
|
||||
try:
|
||||
response = requests.get(final_url, headers=headers, timeout=1)
|
||||
final_url = response.url # Get the final URL after any redirects
|
||||
|
||||
# check for redirected url also
|
||||
validate_url_ip(final_url)
|
||||
response = requests.get(url, headers=headers, timeout=1)
|
||||
|
||||
soup = BeautifulSoup(response.content, "html.parser")
|
||||
title_tag = soup.find("title")
|
||||
@@ -83,8 +60,8 @@ def crawl_work_item_link_title_and_favicon(url: str) -> Dict[str, Any]:
|
||||
except requests.RequestException as e:
|
||||
logger.warning(f"Failed to fetch HTML for title: {str(e)}")
|
||||
|
||||
# Fetch and encode favicon using final URL (after redirects)
|
||||
favicon_base64 = fetch_and_encode_favicon(headers, soup, final_url)
|
||||
# Fetch and encode favicon
|
||||
favicon_base64 = fetch_and_encode_favicon(headers, soup, url)
|
||||
|
||||
# Prepare result
|
||||
result = {
|
||||
|
||||
@@ -21,7 +21,7 @@ from plane.db.models import (
|
||||
WorkspaceMember,
|
||||
Project,
|
||||
ProjectMember,
|
||||
ProjectUserProperty,
|
||||
IssueUserProperty,
|
||||
State,
|
||||
Label,
|
||||
Issue,
|
||||
@@ -94,7 +94,7 @@ def create_project_and_member(workspace: Workspace, bot_user: User) -> Dict[int,
|
||||
project_seed.pop("name", None)
|
||||
project_seed.pop("identifier", None)
|
||||
|
||||
project = Project(
|
||||
project = Project.objects.create(
|
||||
**project_seed,
|
||||
workspace=workspace,
|
||||
name=workspace.name, # Use workspace name
|
||||
@@ -105,7 +105,6 @@ def create_project_and_member(workspace: Workspace, bot_user: User) -> Dict[int,
|
||||
module_view=True,
|
||||
issue_views_view=True,
|
||||
)
|
||||
project.save(created_by_id=bot_user.id, disable_auto_set_user=True)
|
||||
|
||||
# Create project members
|
||||
ProjectMember.objects.bulk_create(
|
||||
@@ -122,9 +121,9 @@ def create_project_and_member(workspace: Workspace, bot_user: User) -> Dict[int,
|
||||
)
|
||||
|
||||
# Create issue user properties
|
||||
ProjectUserProperty.objects.bulk_create(
|
||||
IssueUserProperty.objects.bulk_create(
|
||||
[
|
||||
ProjectUserProperty(
|
||||
IssueUserProperty(
|
||||
project=project,
|
||||
user_id=workspace_member["member_id"],
|
||||
workspace_id=workspace.id,
|
||||
@@ -192,13 +191,13 @@ def create_project_states(
|
||||
state_id = state_seed.pop("id")
|
||||
project_id = state_seed.pop("project_id")
|
||||
|
||||
state = State(
|
||||
state = State.objects.create(
|
||||
**state_seed,
|
||||
project_id=project_map[project_id],
|
||||
workspace=workspace,
|
||||
created_by_id=bot_user.id,
|
||||
)
|
||||
state.save(created_by_id=bot_user.id, disable_auto_set_user=True)
|
||||
|
||||
state_map[state_id] = state.id
|
||||
logger.info(f"Task: workspace_seed_task -> State {state_id} created")
|
||||
return state_map
|
||||
@@ -225,13 +224,12 @@ def create_project_labels(
|
||||
for label_seed in label_seeds:
|
||||
label_id = label_seed.pop("id")
|
||||
project_id = label_seed.pop("project_id")
|
||||
label = Label(
|
||||
label = Label.objects.create(
|
||||
**label_seed,
|
||||
project_id=project_map[project_id],
|
||||
workspace=workspace,
|
||||
created_by_id=bot_user.id,
|
||||
)
|
||||
label.save(created_by_id=bot_user.id, disable_auto_set_user=True)
|
||||
label_map[label_id] = label.id
|
||||
|
||||
logger.info(f"Task: workspace_seed_task -> Label {label_id} created")
|
||||
@@ -278,14 +276,13 @@ def create_project_issues(
|
||||
cycle_id = issue_seed.pop("cycle_id")
|
||||
module_ids = issue_seed.pop("module_ids")
|
||||
|
||||
issue = Issue(
|
||||
issue = Issue.objects.create(
|
||||
**issue_seed,
|
||||
state_id=states_map[state_id],
|
||||
project_id=project_map[project_id],
|
||||
workspace=workspace,
|
||||
created_by_id=bot_user.id,
|
||||
)
|
||||
issue.save(created_by_id=bot_user.id, disable_auto_set_user=True)
|
||||
IssueSequence.objects.create(
|
||||
issue=issue,
|
||||
project_id=project_map[project_id],
|
||||
@@ -354,7 +351,7 @@ def create_pages(workspace: Workspace, project_map: Dict[int, uuid.UUID], bot_us
|
||||
for page_seed in page_seeds:
|
||||
page_id = page_seed.pop("id")
|
||||
|
||||
page = Page(
|
||||
page = Page.objects.create(
|
||||
workspace_id=workspace.id,
|
||||
is_global=False,
|
||||
access=page_seed.get("access", Page.PUBLIC_ACCESS),
|
||||
@@ -368,18 +365,16 @@ def create_pages(workspace: Workspace, project_map: Dict[int, uuid.UUID], bot_us
|
||||
owned_by_id=bot_user.id,
|
||||
)
|
||||
|
||||
page.save(created_by_id=bot_user.id, disable_auto_set_user=True)
|
||||
|
||||
logger.info(f"Task: workspace_seed_task -> Page {page_id} created")
|
||||
if page_seed.get("project_id") and page_seed.get("type") == "PROJECT":
|
||||
project_page = ProjectPage(
|
||||
ProjectPage.objects.create(
|
||||
workspace_id=workspace.id,
|
||||
project_id=project_map[page_seed.get("project_id")],
|
||||
page_id=page.id,
|
||||
created_by_id=bot_user.id,
|
||||
updated_by_id=bot_user.id,
|
||||
)
|
||||
project_page.save(created_by_id=bot_user.id, disable_auto_set_user=True)
|
||||
|
||||
logger.info(f"Task: workspace_seed_task -> Project Page {page_id} created")
|
||||
return
|
||||
|
||||
@@ -419,7 +414,7 @@ def create_cycles(workspace: Workspace, project_map: Dict[int, uuid.UUID], bot_u
|
||||
start_date = timezone.now() + timedelta(days=14)
|
||||
end_date = start_date + timedelta(days=14)
|
||||
|
||||
cycle = Cycle(
|
||||
cycle = Cycle.objects.create(
|
||||
**cycle_seed,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
@@ -428,7 +423,6 @@ def create_cycles(workspace: Workspace, project_map: Dict[int, uuid.UUID], bot_u
|
||||
created_by_id=bot_user.id,
|
||||
owned_by_id=bot_user.id,
|
||||
)
|
||||
cycle.save(created_by_id=bot_user.id, disable_auto_set_user=True)
|
||||
|
||||
cycle_map[cycle_id] = cycle.id
|
||||
logger.info(f"Task: workspace_seed_task -> Cycle {cycle_id} created")
|
||||
@@ -456,7 +450,7 @@ def create_modules(workspace: Workspace, project_map: Dict[int, uuid.UUID], bot_
|
||||
start_date = timezone.now() + timedelta(days=index * 2)
|
||||
end_date = start_date + timedelta(days=14)
|
||||
|
||||
module = Module(
|
||||
module = Module.objects.create(
|
||||
**module_seed,
|
||||
start_date=start_date,
|
||||
target_date=end_date,
|
||||
@@ -464,7 +458,6 @@ def create_modules(workspace: Workspace, project_map: Dict[int, uuid.UUID], bot_
|
||||
workspace=workspace,
|
||||
created_by_id=bot_user.id,
|
||||
)
|
||||
module.save(created_by_id=bot_user.id, disable_auto_set_user=True)
|
||||
module_map[module_id] = module.id
|
||||
logger.info(f"Task: workspace_seed_task -> Module {module_id} created")
|
||||
return module_map
|
||||
@@ -485,15 +478,13 @@ def create_views(workspace: Workspace, project_map: Dict[int, uuid.UUID], bot_us
|
||||
|
||||
for view_seed in view_seeds:
|
||||
project_id = view_seed.pop("project_id")
|
||||
view_seed.pop("id")
|
||||
issue_view = IssueView(
|
||||
IssueView.objects.create(
|
||||
**view_seed,
|
||||
project_id=project_map[project_id],
|
||||
workspace=workspace,
|
||||
created_by_id=bot_user.id,
|
||||
owned_by_id=bot_user.id,
|
||||
)
|
||||
issue_view.save(created_by_id=bot_user.id, disable_auto_set_user=True)
|
||||
|
||||
|
||||
@shared_task
|
||||
@@ -527,14 +518,6 @@ def workspace_seed(workspace_id: uuid.UUID) -> None:
|
||||
is_password_autoset=True,
|
||||
)
|
||||
|
||||
# Add bot user to workspace as member
|
||||
WorkspaceMember.objects.create(
|
||||
workspace=workspace,
|
||||
member=bot_user,
|
||||
role=20,
|
||||
company_role="",
|
||||
)
|
||||
|
||||
# Create a project with the same name as workspace
|
||||
project_map = create_project_and_member(workspace, bot_user)
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from plane.db.models import (
|
||||
WorkspaceMember,
|
||||
ProjectMember,
|
||||
Project,
|
||||
ProjectUserProperty,
|
||||
IssueUserProperty,
|
||||
)
|
||||
|
||||
|
||||
@@ -47,18 +47,27 @@ class Command(BaseCommand):
|
||||
if not WorkspaceMember.objects.filter(workspace=project.workspace, member=user, is_active=True).exists():
|
||||
raise CommandError("User not member in workspace")
|
||||
|
||||
# Get the smallest sort order
|
||||
smallest_sort_order = (
|
||||
ProjectMember.objects.filter(workspace_id=project.workspace_id).order_by("sort_order").first()
|
||||
)
|
||||
|
||||
if smallest_sort_order:
|
||||
sort_order = smallest_sort_order.sort_order - 1000
|
||||
else:
|
||||
sort_order = 65535
|
||||
|
||||
if ProjectMember.objects.filter(project=project, member=user).exists():
|
||||
# Update the project member
|
||||
ProjectMember.objects.filter(project=project, member=user).update(
|
||||
is_active=True, role=role
|
||||
is_active=True, sort_order=sort_order, role=role
|
||||
)
|
||||
else:
|
||||
# Create the project member
|
||||
ProjectMember.objects.create(project=project, member=user, role=role)
|
||||
ProjectMember.objects.create(project=project, member=user, role=role, sort_order=sort_order)
|
||||
|
||||
# Issue Property
|
||||
ProjectUserProperty.objects.get_or_create(user=user, project=project)
|
||||
IssueUserProperty.objects.get_or_create(user=user, project=project)
|
||||
|
||||
# Success message
|
||||
self.stdout.write(self.style.SUCCESS(f"User {user_email} added to project {project_id}"))
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
# Generated by Django 4.2.26 on 2025-12-15 10:29
|
||||
|
||||
from django.db import migrations, models
|
||||
import plane.db.models.workspace
|
||||
|
||||
|
||||
def get_default_product_tour():
|
||||
return {
|
||||
"work_items": True,
|
||||
"cycles": True,
|
||||
"modules": True,
|
||||
"intake": True,
|
||||
"pages": True,
|
||||
}
|
||||
|
||||
|
||||
def populate_product_tour(apps, _schema_editor):
|
||||
WorkspaceUserProperties = apps.get_model('db', 'WorkspaceUserProperties')
|
||||
default_value = get_default_product_tour()
|
||||
# Use bulk update for better performance
|
||||
WorkspaceUserProperties.objects.all().update(product_tour=default_value)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0112_auto_20251124_0603'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='webhook',
|
||||
name='version',
|
||||
field=models.CharField(default='v1', max_length=50),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='is_navigation_tour_completed',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='workspaceuserproperties',
|
||||
name='product_tour',
|
||||
field=models.JSONField(default=plane.db.models.workspace.get_default_product_tour),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='apitoken',
|
||||
name='allowed_rate_limit',
|
||||
field=models.CharField(default='60/min', max_length=255),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='is_subscribed_to_changelog',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.RunPython(populate_product_tour, reverse_code=migrations.RunPython.noop),
|
||||
]
|
||||
-50
@@ -1,50 +0,0 @@
|
||||
# Generated by Django 4.2.22 on 2026-01-05 08:35
|
||||
|
||||
from django.db import migrations, models
|
||||
import plane.db.models.project
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0113_webhook_version'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelTable(
|
||||
name='issueuserproperty',
|
||||
table='project_user_properties',
|
||||
),
|
||||
migrations.RenameModel(
|
||||
old_name='IssueUserProperty',
|
||||
new_name='ProjectUserProperty',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='projectuserproperty',
|
||||
name='preferences',
|
||||
field=models.JSONField(default=plane.db.models.project.get_default_preferences),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='projectuserproperty',
|
||||
name='sort_order',
|
||||
field=models.FloatField(default=65535),
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='projectuserproperty',
|
||||
options={'ordering': ('-created_at',), 'verbose_name': 'Project User Property', 'verbose_name_plural': 'Project User Properties'},
|
||||
),
|
||||
migrations.RemoveConstraint(
|
||||
model_name='projectuserproperty',
|
||||
name='issue_user_property_unique_user_project_when_deleted_at_null',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='projectuserproperty',
|
||||
name='user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_property_user', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='projectuserproperty',
|
||||
constraint=models.UniqueConstraint(condition=models.Q(('deleted_at__isnull', True)), fields=('user', 'project'), name='project_user_property_unique_user_project_when_deleted_at_null'),
|
||||
),
|
||||
]
|
||||
@@ -1,51 +0,0 @@
|
||||
# Generated by Django 4.2.22 on 2026-01-05 08:36
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
def move_issue_user_properties_to_project_user_properties(apps, schema_editor):
|
||||
ProjectMember = apps.get_model('db', 'ProjectMember')
|
||||
ProjectUserProperty = apps.get_model('db', 'ProjectUserProperty')
|
||||
|
||||
# Get all project members
|
||||
project_members = ProjectMember.objects.filter(deleted_at__isnull=True).values('member_id', 'project_id', 'preferences', 'sort_order')
|
||||
|
||||
# create a mapping with consistent ordering
|
||||
pm_dict = {
|
||||
(pm['member_id'], pm['project_id']): pm
|
||||
for pm in project_members
|
||||
}
|
||||
|
||||
# Get all project user properties
|
||||
properties_to_update = []
|
||||
for projectuserproperty in ProjectUserProperty.objects.filter(deleted_at__isnull=True):
|
||||
pm = pm_dict.get((projectuserproperty.user_id, projectuserproperty.project_id))
|
||||
if pm:
|
||||
projectuserproperty.preferences = pm['preferences']
|
||||
projectuserproperty.sort_order = pm['sort_order']
|
||||
properties_to_update.append(projectuserproperty)
|
||||
|
||||
ProjectUserProperty.objects.bulk_update(properties_to_update, ['preferences', 'sort_order'], batch_size=2000)
|
||||
|
||||
|
||||
|
||||
def migrate_existing_api_tokens(apps, schema_editor):
|
||||
APIToken = apps.get_model('db', 'APIToken')
|
||||
|
||||
# Update all the existing non-service api tokens to not have a workspace
|
||||
APIToken.objects.filter(is_service=False, user__is_bot=False).update(
|
||||
workspace_id=None,
|
||||
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0114_projectuserproperty_delete_issueuserproperty_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(move_issue_user_properties_to_project_user_properties, reverse_code=migrations.RunPython.noop),
|
||||
migrations.RunPython(migrate_existing_api_tokens, reverse_code=migrations.RunPython.noop),
|
||||
]
|
||||
@@ -1,38 +0,0 @@
|
||||
# Generated by Django 4.2.27 on 2026-01-13 10:38
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0115_auto_20260105_1406'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='notification_view_mode',
|
||||
field=models.CharField(choices=[('full', 'Full'), ('compact', 'Compact')], default='full', max_length=255),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='is_password_reset_required',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='workspacemember',
|
||||
name='explored_features',
|
||||
field=models.JSONField(default=dict),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='workspacemember',
|
||||
name='getting_started_checklist',
|
||||
field=models.JSONField(default=dict),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='workspacemember',
|
||||
name='tips',
|
||||
field=models.JSONField(default=dict),
|
||||
),
|
||||
]
|
||||
@@ -34,6 +34,7 @@ from .issue import (
|
||||
IssueLabel,
|
||||
IssueLink,
|
||||
IssueMention,
|
||||
IssueUserProperty,
|
||||
IssueReaction,
|
||||
IssueRelation,
|
||||
IssueSequence,
|
||||
@@ -53,7 +54,6 @@ from .project import (
|
||||
ProjectMemberInvite,
|
||||
ProjectNetwork,
|
||||
ProjectPublicMember,
|
||||
ProjectUserProperty,
|
||||
)
|
||||
from .session import Session
|
||||
from .social_connection import SocialLoginConnection
|
||||
|
||||
@@ -32,7 +32,6 @@ class APIToken(BaseModel):
|
||||
workspace = models.ForeignKey("db.Workspace", related_name="api_tokens", on_delete=models.CASCADE, null=True)
|
||||
expired_at = models.DateTimeField(blank=True, null=True)
|
||||
is_service = models.BooleanField(default=False)
|
||||
allowed_rate_limit = models.CharField(max_length=255, default="60/min")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "API Token"
|
||||
|
||||
@@ -90,6 +90,14 @@ class IssueManager(SoftDeletionManager):
|
||||
return (
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(
|
||||
models.Q(issue_intake__status=1)
|
||||
| models.Q(issue_intake__status=-1)
|
||||
| models.Q(issue_intake__status=2)
|
||||
| models.Q(issue_intake__isnull=True)
|
||||
)
|
||||
.filter(deleted_at__isnull=True)
|
||||
.filter(state__is_triage=False)
|
||||
.exclude(state__group=StateGroup.TRIAGE.value)
|
||||
.exclude(archived_at__isnull=False)
|
||||
.exclude(project__archived_at__isnull=False)
|
||||
@@ -526,6 +534,36 @@ class IssueComment(ChangeTrackerMixin, ProjectBaseModel):
|
||||
return str(self.issue)
|
||||
|
||||
|
||||
class IssueUserProperty(ProjectBaseModel):
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="issue_property_user",
|
||||
)
|
||||
filters = models.JSONField(default=get_default_filters)
|
||||
display_filters = models.JSONField(default=get_default_display_filters)
|
||||
display_properties = models.JSONField(default=get_default_display_properties)
|
||||
rich_filters = models.JSONField(default=dict)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Issue User Property"
|
||||
verbose_name_plural = "Issue User Properties"
|
||||
db_table = "issue_user_properties"
|
||||
ordering = ("-created_at",)
|
||||
unique_together = ["user", "project", "deleted_at"]
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["user", "project"],
|
||||
condition=Q(deleted_at__isnull=True),
|
||||
name="issue_user_property_unique_user_project_when_deleted_at_null",
|
||||
)
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
"""Return properties status of the issue"""
|
||||
return str(self.user)
|
||||
|
||||
|
||||
class IssueLabel(ProjectBaseModel):
|
||||
issue = models.ForeignKey("db.Issue", on_delete=models.CASCADE, related_name="label_issue")
|
||||
label = models.ForeignKey("db.Label", on_delete=models.CASCADE, related_name="label_issue")
|
||||
|
||||
@@ -12,6 +12,7 @@ from django.db.models import Q
|
||||
# Module imports
|
||||
from plane.db.mixins import AuditModel
|
||||
|
||||
# Module imports
|
||||
from .base import BaseModel
|
||||
|
||||
ROLE_CHOICES = ((20, "Admin"), (15, "Member"), (5, "Guest"))
|
||||
@@ -218,20 +219,14 @@ class ProjectMember(ProjectBaseModel):
|
||||
is_active = models.BooleanField(default=True)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self._state.adding and self.member:
|
||||
# Get the minimum sort_order for this member in the workspace
|
||||
min_sort_order_result = ProjectUserProperty.objects.filter(
|
||||
workspace_id=self.project.workspace_id, user=self.member
|
||||
).aggregate(min_sort_order=models.Min("sort_order"))
|
||||
min_sort_order = min_sort_order_result.get("min_sort_order")
|
||||
if self._state.adding:
|
||||
smallest_sort_order = ProjectMember.objects.filter(
|
||||
workspace_id=self.project.workspace_id, member=self.member
|
||||
).aggregate(smallest=models.Min("sort_order"))["smallest"]
|
||||
|
||||
# create project user property with project sort order
|
||||
ProjectUserProperty.objects.create(
|
||||
workspace_id=self.project.workspace_id,
|
||||
project=self.project,
|
||||
user=self.member,
|
||||
sort_order=(min_sort_order - 10000 if min_sort_order is not None else 65535),
|
||||
)
|
||||
# Project ordering
|
||||
if smallest_sort_order is not None:
|
||||
self.sort_order = smallest_sort_order - 10000
|
||||
|
||||
super(ProjectMember, self).save(*args, **kwargs)
|
||||
|
||||
@@ -331,37 +326,3 @@ class ProjectPublicMember(ProjectBaseModel):
|
||||
verbose_name_plural = "Project Public Members"
|
||||
db_table = "project_public_members"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
|
||||
class ProjectUserProperty(ProjectBaseModel):
|
||||
from .issue import get_default_filters, get_default_display_filters, get_default_display_properties
|
||||
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="project_property_user",
|
||||
)
|
||||
filters = models.JSONField(default=get_default_filters)
|
||||
display_filters = models.JSONField(default=get_default_display_filters)
|
||||
display_properties = models.JSONField(default=get_default_display_properties)
|
||||
rich_filters = models.JSONField(default=dict)
|
||||
preferences = models.JSONField(default=get_default_preferences)
|
||||
sort_order = models.FloatField(default=65535)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Project User Property"
|
||||
verbose_name_plural = "Project User Properties"
|
||||
db_table = "project_user_properties"
|
||||
ordering = ("-created_at",)
|
||||
unique_together = ["user", "project", "deleted_at"]
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["user", "project"],
|
||||
condition=Q(deleted_at__isnull=True),
|
||||
name="project_user_property_unique_user_project_when_deleted_at_null",
|
||||
)
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
"""Return properties status of the project"""
|
||||
return str(self.user)
|
||||
|
||||
@@ -5,7 +5,7 @@ from django.db.models import Q
|
||||
|
||||
# Module imports
|
||||
from .project import ProjectBaseModel
|
||||
from plane.db.mixins import SoftDeletionManager
|
||||
|
||||
|
||||
class StateGroup(models.TextChoices):
|
||||
BACKLOG = "backlog", "Backlog"
|
||||
@@ -58,14 +58,14 @@ DEFAULT_STATES = [
|
||||
]
|
||||
|
||||
|
||||
class StateManager(SoftDeletionManager):
|
||||
class StateManager(models.Manager):
|
||||
"""Default manager - excludes triage states"""
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().exclude(group=StateGroup.TRIAGE.value)
|
||||
|
||||
|
||||
class TriageStateManager(SoftDeletionManager):
|
||||
class TriageStateManager(models.Manager):
|
||||
"""Manager for triage states only"""
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
@@ -84,7 +84,7 @@ class User(AbstractBaseUser, PermissionsMixin):
|
||||
is_staff = models.BooleanField(default=False)
|
||||
is_email_verified = models.BooleanField(default=False)
|
||||
is_password_autoset = models.BooleanField(default=False)
|
||||
is_password_reset_required = models.BooleanField(default=False)
|
||||
|
||||
# random token generated
|
||||
token = models.CharField(max_length=64, blank=True)
|
||||
|
||||
@@ -192,10 +192,6 @@ class Profile(TimeAuditModel):
|
||||
FRIDAY = 5
|
||||
SATURDAY = 6
|
||||
|
||||
class NotificationViewMode(models.TextChoices):
|
||||
FULL = "full", "Full"
|
||||
COMPACT = "compact", "Compact"
|
||||
|
||||
START_OF_THE_WEEK_CHOICES = (
|
||||
(SUNDAY, "Sunday"),
|
||||
(MONDAY, "Monday"),
|
||||
@@ -225,9 +221,7 @@ class Profile(TimeAuditModel):
|
||||
billing_address = models.JSONField(null=True)
|
||||
has_billing_address = models.BooleanField(default=False)
|
||||
company_name = models.CharField(max_length=255, blank=True)
|
||||
notification_view_mode = models.CharField(
|
||||
max_length=255, choices=NotificationViewMode.choices, default=NotificationViewMode.FULL
|
||||
)
|
||||
|
||||
is_smooth_cursor_enabled = models.BooleanField(default=False)
|
||||
# mobile
|
||||
is_mobile_onboarded = models.BooleanField(default=False)
|
||||
@@ -239,12 +233,8 @@ class Profile(TimeAuditModel):
|
||||
goals = models.JSONField(default=dict)
|
||||
background_color = models.CharField(max_length=255, default=get_random_color)
|
||||
|
||||
# navigation tour
|
||||
is_navigation_tour_completed = models.BooleanField(default=False)
|
||||
|
||||
# marketing
|
||||
has_marketing_email_consent = models.BooleanField(default=False)
|
||||
is_subscribed_to_changelog = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Profile"
|
||||
|
||||
@@ -38,7 +38,6 @@ class Webhook(BaseModel):
|
||||
cycle = models.BooleanField(default=False)
|
||||
issue_comment = models.BooleanField(default=False)
|
||||
is_internal = models.BooleanField(default=False)
|
||||
version = models.CharField(default="v1", max_length=50)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.workspace.slug} {self.url}"
|
||||
|
||||
@@ -112,16 +112,6 @@ def slug_validator(value):
|
||||
raise ValidationError("Slug is not valid")
|
||||
|
||||
|
||||
def get_default_product_tour():
|
||||
return {
|
||||
"work_items": False,
|
||||
"cycles": False,
|
||||
"modules": False,
|
||||
"intake": False,
|
||||
"pages": False,
|
||||
}
|
||||
|
||||
|
||||
class Workspace(BaseModel):
|
||||
TIMEZONE_CHOICES = tuple(zip(pytz.common_timezones, pytz.common_timezones))
|
||||
|
||||
@@ -214,9 +204,6 @@ class WorkspaceMember(BaseModel):
|
||||
default_props = models.JSONField(default=get_default_props)
|
||||
issue_props = models.JSONField(default=get_issue_props)
|
||||
is_active = models.BooleanField(default=True)
|
||||
getting_started_checklist = models.JSONField(default=dict)
|
||||
tips = models.JSONField(default=dict)
|
||||
explored_features = models.JSONField(default=dict)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["workspace", "member", "deleted_at"]
|
||||
@@ -338,7 +325,6 @@ class WorkspaceUserProperties(BaseModel):
|
||||
choices=NavigationControlPreference.choices,
|
||||
default=NavigationControlPreference.ACCORDION,
|
||||
)
|
||||
product_tour = models.JSONField(default=get_default_product_tour)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["workspace", "user", "deleted_at"]
|
||||
|
||||
@@ -6,7 +6,7 @@ from django.utils import timezone
|
||||
from plane.db.models import (
|
||||
Project,
|
||||
ProjectMember,
|
||||
ProjectUserProperty,
|
||||
IssueUserProperty,
|
||||
State,
|
||||
WorkspaceMember,
|
||||
User,
|
||||
@@ -82,8 +82,8 @@ class TestProjectAPIPost(TestProjectBase):
|
||||
assert project_member.role == 20 # Administrator
|
||||
assert project_member.is_active is True
|
||||
|
||||
# Verify ProjectUserProperty was created
|
||||
assert ProjectUserProperty.objects.filter(project=project, user=user).exists()
|
||||
# Verify IssueUserProperty was created
|
||||
assert IssueUserProperty.objects.filter(project=project, user=user).exists()
|
||||
|
||||
# Verify default states were created
|
||||
states = State.objects.filter(project=project)
|
||||
@@ -116,8 +116,8 @@ class TestProjectAPIPost(TestProjectBase):
|
||||
project = Project.objects.get(name=project_data["name"])
|
||||
assert ProjectMember.objects.filter(project=project, role=20).count() == 2
|
||||
|
||||
# Verify both have ProjectUserProperty
|
||||
assert ProjectUserProperty.objects.filter(project=project).count() == 2
|
||||
# Verify both have IssueUserProperty
|
||||
assert IssueUserProperty.objects.filter(project=project).count() == 2
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_project_guest_forbidden(self, session_client, workspace):
|
||||
|
||||
@@ -51,7 +51,7 @@ def get_x_axis_field() -> Dict[str, Tuple[str, str, Optional[Dict[str, Any]]]]:
|
||||
"assignees__display_name",
|
||||
{"issue_assignee__deleted_at__isnull": True},
|
||||
),
|
||||
"ESTIMATE_POINTS": ("estimate_point__key", "estimate_point__value", None),
|
||||
"ESTIMATE_POINTS": ("estimate_point__value", "estimate_point__key", None),
|
||||
"CYCLES": (
|
||||
"issue_cycle__cycle_id",
|
||||
"issue_cycle__cycle__name",
|
||||
|
||||
@@ -1,306 +1,330 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office" >
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Reset your Plane password</title>
|
||||
|
||||
<style>
|
||||
/* Basic resets for email clients */
|
||||
body,
|
||||
table,
|
||||
td,
|
||||
p {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
line-height: 100%;
|
||||
text-decoration: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
/* Mobile adjustments */
|
||||
@media only screen and (max-width: 600px) {
|
||||
.email-container {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.email-content {
|
||||
padding: 24px 20px !important;
|
||||
}
|
||||
|
||||
.email-footer {
|
||||
padding: 0 20px 16px 20px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Set a new password to your Plane account</title>
|
||||
<style type="text/css" emogrify="no"> #outlook a { padding: 0; } .ExternalClass { width: 100%; } .ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div { line-height: 100%; } table td { border-collapse: collapse; mso-line-height-rule: exactly; } .editable.image { font-size: 0 !important; line-height: 0 !important; } .nl2go_preheader { display: none !important; mso-hide: all !important; mso-line-height-rule: exactly; visibility: hidden !important; line-height: 0px !important; font-size: 0px !important; } body { width: 100% !important; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; margin: 0; padding: 0; } img { outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; } a img { border: none; } table { border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; } th { font-weight: normal; text-align: left; } *[class="gmail-fix"] { display: none !important; } </style>
|
||||
<style type="text/css" emogrify="no"> @media (max-width: 600px) { .gmx-killpill { content: " \03D1"; } } </style>
|
||||
<style type="text/css" emogrify="no"> @media (max-width: 600px) { .gmx-killpill { content: " \03D1"; } .r0-o { border-style: solid !important; margin: 0 auto 0 auto !important; width: 320px !important; } .r1-i { background-color: #ffffff !important; } .r2-c { box-sizing: border-box !important; text-align: center !important; valign: top !important; width: 100% !important; } .r3-o { border-style: solid !important; margin: 0 auto 0 auto !important; width: 100% !important; } .r4-i { padding-bottom: 20px !important; padding-left: 15px !important; padding-right: 15px !important; padding-top: 20px !important; } .r5-c { box-sizing: border-box !important; display: block !important; valign: top !important; width: 100% !important; } .r6-o { border-style: solid !important; width: 100% !important; } .r7-i { padding-left: 0px !important; padding-right: 0px !important; } .r8-c { box-sizing: border-box !important; text-align: left !important; valign: top !important; width: 100% !important; } .r9-o { border-style: solid !important; margin: 0 auto 0 0 !important; width: 100% !important; } .r10-i { padding-bottom: 10px !important; padding-left: 0px !important; padding-right: 0px !important; padding-top: 10px !important; } .r11-i { padding-bottom: 15px !important; padding-top: 15px !important; text-align: left !important; } .r12-i { background-color: #ffffff !important; padding-bottom: 15px !important; padding-top: 15px !important; text-align: left !important; } .r13-c { box-sizing: border-box !important; padding: 0 !important; text-align: center !important; valign: top !important; width: 100% !important; } .r14-o { border-style: solid !important; margin: 0 auto 0 auto !important; margin-bottom: 15px !important; margin-top: 15px !important; width: 100% !important; } .r15-i { padding: 0 !important; text-align: center !important; } .r16-r { background-color: #006399 !important; border-radius: 4px !important; border-width: 0px !important; box-sizing: border-box; height: initial !important; padding: 0 !important; padding-bottom: 12px !important; padding-top: 12px !important; text-align: center !important; width: 100% !important; } .r17-i { padding-bottom: 0px !important; padding-left: 15px !important; padding-right: 15px !important; padding-top: 0px !important; } .r18-c { box-sizing: border-box !important; text-align: center !important; width: 100% !important; } .r19-i { padding-bottom: 5px !important; padding-top: 5px !important; } .r20-i { padding-bottom: 15px !important; padding-left: 15px !important; padding-right: 15px !important; padding-top: 15px !important; } .r21-o { border-bottom-color: #efefef !important; border-bottom-width: 2px !important; border-left-color: #efefef !important; border-left-width: 2px !important; border-right-color: #efefef !important; border-right-width: 2px !important; border-style: solid !important; border-top-color: #efefef !important; border-top-width: 2px !important; margin: 0 auto 0 0 !important; width: 100% !important; } .r22-i { padding-bottom: 5px !important; padding-left: 5px !important; padding-right: 5px !important; padding-top: 5px !important; text-align: left !important; } .r23-i { padding-bottom: 5px !important; padding-left: 15px !important; padding-right: 15px !important; padding-top: 5px !important; } .r24-c { box-sizing: border-box !important; width: 100% !important; } .r25-i { font-size: 0px !important; padding-bottom: 10px !important; padding-left: 65px !important; padding-right: 65px !important; padding-top: 10px !important; } .r26-c { box-sizing: border-box !important; width: 32px !important; } .r27-o { border-style: solid !important; margin-right: 8px !important; width: 32px !important; } .r28-o { border-style: solid !important; margin-right: 0px !important; width: 32px !important; } .r29-i { padding-bottom: 0px !important; padding-top: 5px !important; text-align: center !important; } body { -webkit-text-size-adjust: none; } .nl2go-responsive-hide { display: none; } .nl2go-body-table { min-width: unset !important; } .mobshow { height: auto !important; overflow: visible !important; max-height: unset !important; visibility: visible !important; border: none !important; } .resp-table { display: inline-table !important; } .magic-resp { display: table-cell !important; } } </style>
|
||||
<style type="text/css"> p, h1, h2, h3, h4, ol, ul { margin: 0; } a, a:link { color: #006399; text-decoration: underline; } .nl2go-default-textstyle { color: #3b3f44; font-family: georgia, serif; font-size: 16px; line-height: 1.5; word-break: break-word; } .default-button { color: #ffffff; font-family: georgia, serif; font-size: 16px; font-style: normal; font-weight: normal; line-height: 1.15; text-decoration: none; word-break: break-word; } .default-heading1 { color: #1f2d3d; font-family: arial, helvetica, sans-serif; font-size: 36px; word-break: break-word; } .default-heading2 { color: #1f2d3d; font-family: arial, helvetica, sans-serif; font-size: 32px; word-break: break-word; } .default-heading3 { color: #1f2d3d; font-family: arial, helvetica, sans-serif; font-size: 24px; word-break: break-word; } .default-heading4 { color: #1f2d3d; font-family: arial, helvetica, sans-serif; font-size: 18px; word-break: break-word; } a[x-apple-data-detectors] { color: inherit !important; text-decoration: inherit !important; font-size: inherit !important; font-family: inherit !important; font-weight: inherit !important; line-height: inherit !important; } .no-show-for-you { border: none; display: none; float: none; font-size: 0; height: 0; line-height: 0; max-height: 0; mso-hide: all; overflow: hidden; table-layout: fixed; visibility: hidden; width: 0; } </style>
|
||||
<!--[if mso ]>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:AllowPNG />
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
<! [endif]-->
|
||||
<style type="text/css"> a:link { color: #006399; text-decoration: underline; } </style>
|
||||
</head>
|
||||
|
||||
<body
|
||||
bgcolor="#f9f9f9"
|
||||
style="
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI',
|
||||
sans-serif;
|
||||
font-weight: 400;
|
||||
color: #474747;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-text-size-adjust: none;
|
||||
background-color: #f9f9f9;
|
||||
"
|
||||
>
|
||||
<!-- Preheader (hidden) -->
|
||||
<span
|
||||
class="preheader"
|
||||
style="
|
||||
display: none;
|
||||
font-size: 1px;
|
||||
color: transparent;
|
||||
line-height: 1px;
|
||||
max-height: 0;
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
mso-hide: all;
|
||||
visibility: hidden;
|
||||
"
|
||||
>
|
||||
Reset your Plane password with this secure link.
|
||||
</span>
|
||||
|
||||
<!-- Outer Wrapper -->
|
||||
<center
|
||||
class="email-body"
|
||||
style="width: 100%; background-color: #f9f9f9; padding: 40px 0;"
|
||||
>
|
||||
<!-- HEADER -->
|
||||
<table role="presentation" width="100%">
|
||||
<tr>
|
||||
<td></td>
|
||||
|
||||
<td
|
||||
class="email-container"
|
||||
width="620"
|
||||
style="width: 620px; max-width: 620px; margin: 0 auto;"
|
||||
>
|
||||
<table
|
||||
role="presentation"
|
||||
width="100%"
|
||||
style="background-color: #000000;"
|
||||
>
|
||||
<tr>
|
||||
<td style="padding: 24px 40px; text-align: center;">
|
||||
<img
|
||||
src="https://media.docs.plane.so/logo/new-logo-dark.png"
|
||||
width="120"
|
||||
alt="Plane"
|
||||
style="height: auto; display: block; margin: 0 auto;"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
<td></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- BODY (card under header) -->
|
||||
<table role="presentation" width="100%">
|
||||
<tr>
|
||||
<td></td>
|
||||
|
||||
<td
|
||||
class="email-container"
|
||||
width="620"
|
||||
style="width: 620px; max-width: 620px; margin: 0 auto;"
|
||||
>
|
||||
<table
|
||||
role="presentation"
|
||||
width="100%"
|
||||
class="email-content"
|
||||
style="background-color: #ffffff; padding: 32px 40px 32px 40px;"
|
||||
>
|
||||
<tr>
|
||||
<td
|
||||
style="
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
color: #474747;
|
||||
padding: 24px;
|
||||
"
|
||||
>
|
||||
<!-- ========================================= -->
|
||||
<!-- PLANE RESET PASSWORD EMAIL -->
|
||||
<!-- ========================================= -->
|
||||
|
||||
<!-- Heading -->
|
||||
<h1
|
||||
style="
|
||||
margin: 0 0 12px;
|
||||
font-size: 22px;
|
||||
line-height: 1.35;
|
||||
font-weight: 600;
|
||||
color: #111111;
|
||||
"
|
||||
>
|
||||
Reset your Plane password
|
||||
</h1>
|
||||
|
||||
<!-- Intro copy -->
|
||||
<p style="margin: 0 0 16px;">
|
||||
We received a request to reset your Plane password. To reset
|
||||
your password, click the button below.
|
||||
</p>
|
||||
|
||||
<!-- Primary button -->
|
||||
<table
|
||||
role="presentation"
|
||||
cellspacing="0"
|
||||
cellpadding="0"
|
||||
border="0"
|
||||
style="margin: 0 0 24px;"
|
||||
>
|
||||
<tr>
|
||||
<td>
|
||||
<a
|
||||
href="{{forgot_password_url}}"
|
||||
target="_blank"
|
||||
style="
|
||||
background-color: #1080bc;
|
||||
border-radius: 6px;
|
||||
color: #ffffff;
|
||||
padding: 12px 24px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
"
|
||||
>
|
||||
Reset password
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Fallback link -->
|
||||
<p
|
||||
style="
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: #5f5e5e;
|
||||
"
|
||||
>
|
||||
If the button doesn’t work, copy and paste this link into
|
||||
your browser:
|
||||
</p>
|
||||
|
||||
<p
|
||||
style="
|
||||
margin: 6px 0 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: #5f5e5e;
|
||||
"
|
||||
>
|
||||
<span
|
||||
style="
|
||||
display: inline-block;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 12px;
|
||||
background-color: #f3f3f3;
|
||||
padding: 4px 6px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #e9e9e9;
|
||||
word-break: break-all;
|
||||
"
|
||||
>{{forgot_password_url}}</span
|
||||
>
|
||||
</p>
|
||||
|
||||
<!-- Note -->
|
||||
<p
|
||||
style="
|
||||
margin: 16px 0 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: #5f5e5e;
|
||||
"
|
||||
>
|
||||
Note: This email was sent to
|
||||
<span
|
||||
style="
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 12px;
|
||||
background-color: #f3f3f3;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #e9e9e9;
|
||||
"
|
||||
>{{email}}</span
|
||||
>. If you weren’t expecting a password reset, you can safely
|
||||
ignore this email.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
<td></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- FOOTER (simple, aligned with body) -->
|
||||
<table role="presentation" width="100%">
|
||||
<tr>
|
||||
<td></td>
|
||||
|
||||
<td
|
||||
class="email-container"
|
||||
width="620"
|
||||
style="width: 620px; max-width: 620px; margin: 0 auto;"
|
||||
>
|
||||
<table
|
||||
role="presentation"
|
||||
width="100%"
|
||||
class="email-footer"
|
||||
style="background-color: #ffffff; padding: 0 40px 16px 40px;"
|
||||
>
|
||||
<tr>
|
||||
<td
|
||||
style="
|
||||
border-top: 1px solid #e9e9e9;
|
||||
padding: 24px;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
color: #979797;
|
||||
text-align: left;
|
||||
"
|
||||
>
|
||||
<p style="margin: 0;">
|
||||
Plane Software, Inc.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
<td></td>
|
||||
</tr>
|
||||
</table>
|
||||
</center>
|
||||
|
||||
<base target="_blank" />
|
||||
<body bgcolor="#ffffff" text="#3b3f44" link="#006399" yahoo="fix" style="background-color: #ffffff" >
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" class="nl2go-body-table" width="100%" style="background-color: #ffffff; width: 100%" >
|
||||
<tr>
|
||||
<td>
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="600" align="center" class="r0-o" style="table-layout: fixed; width: 600px" >
|
||||
<tr>
|
||||
<td valign="top" class="r1-i" style="background-color: #ffffff">
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" align="center" class="r3-o" style="table-layout: fixed; width: 100%" >
|
||||
<tr>
|
||||
<td class="r4-i" style="padding-bottom: 20px; padding-top: 20px" >
|
||||
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
|
||||
<tr>
|
||||
<th width="100%" valign="top" class="r5-c" style="font-weight: normal" >
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r6-o" style="table-layout: fixed; width: 100%" >
|
||||
<tr>
|
||||
<td valign="top" class="r7-i" style=" padding-left: 10px; padding-right: 10px; " >
|
||||
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
|
||||
<tr>
|
||||
<td class="r8-c" align="left">
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="150" class="r9-o" style=" table-layout: fixed; width: 150px; " >
|
||||
<tr>
|
||||
<td style=" font-size: 0px; line-height: 0px; " > <img src="https://media.docs.plane.so/logo/new-logo-white.png" width="150" border="0" style=" display: block; width: 100%; " /> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</th>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" align="center" class="r3-o" style="table-layout: fixed; width: 100%" >
|
||||
<tr>
|
||||
<td class="r10-i" style="padding-bottom: 10px; padding-top: 10px" >
|
||||
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
|
||||
<tr>
|
||||
<th width="100%" valign="top" class="r5-c" style="font-weight: normal" >
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r6-o" style="table-layout: fixed; width: 100%" >
|
||||
<tr>
|
||||
<td valign="top" class="r7-i" style=" padding-left: 15px; padding-right: 15px; " >
|
||||
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
|
||||
<tr>
|
||||
<td class="r8-c" align="left">
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r9-o" style=" table-layout: fixed; width: 100%; " >
|
||||
<tr>
|
||||
<td align="left" valign="top" class="r11-i nl2go-default-textstyle" style=" color: #3b3f44; font-family: georgia, serif; font-size: 16px; word-break: break-word; line-height: 1.5; padding-bottom: 15px; padding-top: 15px; text-align: left; " >
|
||||
<div>
|
||||
<p style="margin: 0"> <span style=" font-family: Arial, helvetica, sans-serif; font-size: 32px; " ><strong >Reset your Plane account's password</strong ></span > </p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="r8-c" align="left">
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r9-o" style=" table-layout: fixed; width: 100%; " >
|
||||
<tr>
|
||||
<td align="left" valign="top" class="r12-i nl2go-default-textstyle" style=" color: #3b3f44; font-family: arial, helvetica, sans-serif; font-size: 16px; line-height: 1.5; word-break: break-word; background-color: #ffffff; padding-bottom: 15px; padding-top: 15px; text-align: left; " >
|
||||
<div>
|
||||
<p style="margin: 0"> <span style="font-size: 17px" >Someone, hopefully you, has requested a new password be set to your Plane account. If it was you, please click the button below to reset your password.</span > </p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="r13-c" align="center" style=" align: center; padding-bottom: 15px; padding-top: 15px; valign: top; " >
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="285" class="r14-o" style=" background-color: #006399; border-collapse: separate; border-color: #006399; border-radius: 4px; border-style: solid; border-width: 0px; table-layout: fixed; width: 285px; " >
|
||||
<tr>
|
||||
<td height="18" align="center" valign="top" class="r15-i nl2go-default-textstyle" style=" word-break: break-word; background-color: #006399; border-radius: 4px; color: #ffffff; font-family: georgia, serif; font-size: 16px; font-style: normal; line-height: 1.15; padding-bottom: 12px; padding-top: 12px; text-align: center; " > <a href="{{forgot_password_url}}" class="r16-r default-button" target="_blank" data-btn="1" style=" font-style: normal; font-weight: normal; line-height: 1.15; text-decoration: none; word-break: break-word; word-wrap: break-word; display: block; -webkit-text-size-adjust: none; color: #ffffff; font-family: arial, helvetica, sans-serif; font-size: 16px; " > <span>Reset password</span></a > </td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="r8-c" align="left">
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r9-o" style=" table-layout: fixed; width: 100%; " >
|
||||
<tr>
|
||||
<td align="left" valign="top" class="r11-i nl2go-default-textstyle" style=" color: #3b3f44; font-family: arial, helvetica, sans-serif; font-size: 16px; line-height: 1.5; word-break: break-word; padding-bottom: 15px; padding-top: 15px; text-align: left; " >
|
||||
<div>
|
||||
<p style="margin: 0"> <span style="font-size: 17px" >Ignore this if you didn't ask for a new link.</span > </p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</th>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" align="center" class="r3-o" style="table-layout: fixed; width: 100%" >
|
||||
<tr>
|
||||
<td class="r17-i">
|
||||
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
|
||||
<tr>
|
||||
<th width="100%" valign="top" class="r5-c" style="font-weight: normal" >
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r6-o" style="table-layout: fixed; width: 100%" >
|
||||
<tr>
|
||||
<td valign="top" class="r7-i" style=" padding-left: 15px; padding-right: 15px; " >
|
||||
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
|
||||
<tr>
|
||||
<td class="r18-c" align="center">
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="570" class="r3-o" style="table-layout: fixed" >
|
||||
<tr>
|
||||
<td class="r19-i" style=" padding-bottom: 5px; padding-top: 5px; height: 1px; " >
|
||||
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
|
||||
<tr>
|
||||
<td>
|
||||
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" valign="" class="r19-i" height="1" style=" border-top-style: solid; background-clip: border-box; border-top-color: #efefef; border-top-width: 1px; font-size: 1px; line-height: 1px; " >
|
||||
<tr>
|
||||
<td height="0" style=" font-size: 0px; line-height: 0px; " > </td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</th>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" align="center" class="r3-o" style="table-layout: fixed; width: 100%" >
|
||||
<tr>
|
||||
<td class="r20-i" style="padding-bottom: 15px; padding-top: 15px" >
|
||||
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
|
||||
<tr>
|
||||
<th width="100%" valign="top" class="r5-c" style="font-weight: normal" >
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r6-o" style="table-layout: fixed; width: 100%" >
|
||||
<tr>
|
||||
<td valign="top" class="r7-i" style=" padding-left: 15px; padding-right: 15px; " >
|
||||
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
|
||||
<tr>
|
||||
<td class="r8-c" align="left">
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r21-o" style=" table-layout: fixed; width: 100%; " >
|
||||
<tr class="nl2go-responsive-hide">
|
||||
<td height="2" width="2" style=" font-size: 2px; line-height: 2px; background-color: #efefef; " > </td>
|
||||
<td height="2" style=" font-size: 2px; line-height: 2px; background-color: #efefef; " > </td>
|
||||
<td height="2" width="2" style=" font-size: 2px; line-height: 2px; background-color: #efefef; " > </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="nl2go-responsive-hide" width="2" style=" font-size: 0px; line-height: 1px; background-color: #efefef; " > </td>
|
||||
<td align="left" valign="top" class="r22-i nl2go-default-textstyle" style=" color: #3b3f44; font-family: georgia, serif; font-size: 16px; word-break: break-word; line-height: 1.5; padding-bottom: 5px; padding-left: 5px; padding-right: 5px; padding-top: 5px; text-align: left; " >
|
||||
<div>
|
||||
<p style="margin: 0"> <span style="font-size: 13px" >Despite our popularity, we are humbly early-stage. We are shipping fast, so please reach out to us with feature requests, major and minor nits, and anything else you find missing. We read every </span ><a href="https://discord.com/channels/1031547764020084846/1094927053867995176" title="Plane Support on Discod" target="_blank" style=" color: #006399; text-decoration: underline; " ><span style="font-size: 13px" >message</span ></a ><span style="font-size: 13px" >, </span ><a href="http://twitter.com/planepowers" title="@planepowers" target="_blank" style=" color: #006399; text-decoration: underline; " ><span style="font-size: 13px" >tweet</span ></a ><span style="font-size: 13px" >, and </span ><a href="https://github.com/makeplane/plane/issues" title="Plane's GitHub conversations" target="_blank" style=" color: #006399; text-decoration: underline; " ><span style="font-size: 13px" >conversation</span ></a ><span style="font-size: 13px" > and update </span ><a href="https://plane.sh/plane/0b170a1c-0e55-47cb-9307-ea49a05672b5?board=kanban" title="Plane's roadmap" target="_blank" style=" color: #006399; text-decoration: underline; " ><span style="font-size: 13px" >our public roadmap</span ></a ><span style="font-size: 13px" >.</span > </p>
|
||||
</div>
|
||||
</td>
|
||||
<td class="nl2go-responsive-hide" width="2" style=" font-size: 0px; line-height: 1px; background-color: #efefef; " > </td>
|
||||
</tr>
|
||||
<tr class="nl2go-responsive-hide">
|
||||
<td height="2" width="2" style=" font-size: 2px; line-height: 2px; background-color: #efefef; " > </td>
|
||||
<td height="2" style=" font-size: 2px; line-height: 2px; background-color: #efefef; " > </td>
|
||||
<td height="2" width="2" style=" font-size: 2px; line-height: 2px; background-color: #efefef; " > </td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</th>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" align="center" class="r3-o" style="table-layout: fixed; width: 100%" >
|
||||
<tr>
|
||||
<td class="r23-i" style="padding-bottom: 5px; padding-top: 5px" >
|
||||
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
|
||||
<tr>
|
||||
<th width="100%" valign="top" class="r5-c" style="font-weight: normal" >
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r6-o" style="table-layout: fixed; width: 100%" >
|
||||
<tr>
|
||||
<td valign="top" class="r7-i" style=" padding-left: 15px; padding-right: 15px; " >
|
||||
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
|
||||
<tr>
|
||||
<td class="r18-c" align="center">
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="570" align="center" class="r3-o" style=" table-layout: fixed; width: 570px; " >
|
||||
<tr>
|
||||
<td valign="top">
|
||||
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
|
||||
<tr>
|
||||
<td class="r24-c" style=" display: inline-block; " >
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="570" class="r6-o" style=" table-layout: fixed; width: 570px; " >
|
||||
<tr>
|
||||
<td class="r25-i" style=" padding-bottom: 10px; padding-left: 209px; padding-right: 209px; padding-top: 10px; " >
|
||||
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
|
||||
<tr>
|
||||
<th width="40" class="r26-c mobshow resp-table" style=" font-weight: normal; " >
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r27-o" style=" table-layout: fixed; width: 100%; " >
|
||||
<tr>
|
||||
<td class="r19-i" style=" font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px; " > <a href="https://github.com/makeplane" target="_blank" style=" color: #006399; text-decoration: underline; " > <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/github_32px.png" width="32" border="0" style=" display: block; width: 100%; " /></a> </td>
|
||||
<td class="nl2go-responsive-hide" width="8" style=" font-size: 0px; line-height: 1px; " > </td>
|
||||
</tr>
|
||||
</table>
|
||||
</th>
|
||||
<th width="40" class="r26-c mobshow resp-table" style=" font-weight: normal; " >
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r27-o" style=" table-layout: fixed; width: 100%; " >
|
||||
<tr>
|
||||
<td class="r19-i" style=" font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px; " > <a href="https://www.linkedin.com/company/planepowers/" target="_blank" style=" color: #006399; text-decoration: underline; " > <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/linkedin_32px.png" width="32" border="0" style=" display: block; width: 100%; " /></a> </td>
|
||||
<td class="nl2go-responsive-hide" width="8" style=" font-size: 0px; line-height: 1px; " > </td>
|
||||
</tr>
|
||||
</table>
|
||||
</th>
|
||||
<th width="40" class="r26-c mobshow resp-table" style=" font-weight: normal; " >
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r27-o" style=" table-layout: fixed; width: 100%; " >
|
||||
<tr>
|
||||
<td class="r19-i" style=" font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px; " > <a href="https://twitter.com/planepowers" target="_blank" style=" color: #006399; text-decoration: underline; " > <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/twitter_32px.png" width="32" border="0" style=" display: block; width: 100%; " /></a> </td>
|
||||
<td class="nl2go-responsive-hide" width="8" style=" font-size: 0px; line-height: 1px; " > </td>
|
||||
</tr>
|
||||
</table>
|
||||
</th>
|
||||
<th width="32" class="r26-c mobshow resp-table" style=" font-weight: normal; " >
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r28-o" style=" table-layout: fixed; width: 100%; " >
|
||||
<tr>
|
||||
<td class="r19-i" style=" font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px; " > <a href="https://plane.so/" target="_blank" style=" color: #006399; text-decoration: underline; " > <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/website_32px.png" width="32" border="0" style=" display: block; width: 100%; " /></a> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</th>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</th>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" align="center" class="r3-o" style="table-layout: fixed; width: 100%" >
|
||||
<tr>
|
||||
<td class="r23-i" style="padding-bottom: 5px; padding-top: 5px" >
|
||||
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
|
||||
<tr>
|
||||
<th width="100%" valign="top" class="r5-c" style="font-weight: normal" >
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r6-o" style="table-layout: fixed; width: 100%" >
|
||||
<tr>
|
||||
<td valign="top" class="r7-i" style=" padding-left: 15px; padding-right: 15px; " >
|
||||
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
|
||||
<tr>
|
||||
<td class="r8-c" align="left">
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r9-o" style=" table-layout: fixed; width: 100%; " >
|
||||
<tr>
|
||||
<td align="center" valign="top" class="r29-i nl2go-default-textstyle" style=" color: #3b3f44; font-family: georgia, serif; word-break: break-word; font-size: 18px; line-height: 1.5; padding-top: 5px; text-align: center; " >
|
||||
<div>
|
||||
<p style=" margin: 0; font-size: 14px; " > <span style="font-size: 12px" >This email was sent to {{email}}. Please delete if you aren't the intended recipient.</span > </p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</th>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,265 +1,288 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office" >
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Your Plane login code</title>
|
||||
|
||||
<style>
|
||||
/* Basic resets for email clients */
|
||||
body,
|
||||
table,
|
||||
td,
|
||||
p {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
line-height: 100%;
|
||||
text-decoration: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
/* Mobile adjustments */
|
||||
@media only screen and (max-width: 600px) {
|
||||
.email-container {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.email-content {
|
||||
padding: 24px 20px !important;
|
||||
}
|
||||
|
||||
.email-footer {
|
||||
padding: 0 20px 16px 20px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Your unique Plane login code is code</title>
|
||||
<style type="text/css" emogrify="no"> #outlook a { padding: 0; } .ExternalClass { width: 100%; } .ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div { line-height: 100%; } table td { border-collapse: collapse; mso-line-height-rule: exactly; } .editable.image { font-size: 0 !important; line-height: 0 !important; } .nl2go_preheader { display: none !important; mso-hide: all !important; mso-line-height-rule: exactly; visibility: hidden !important; line-height: 0px !important; font-size: 0px !important; } body { width: 100% !important; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; margin: 0; padding: 0; } img { outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; } a img { border: none; } table { border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; } th { font-weight: normal; text-align: left; } *[class="gmail-fix"] { display: none !important; } </style>
|
||||
<style type="text/css" emogrify="no"> @media (max-width: 600px) { .gmx-killpill { content: " \03D1"; } } </style>
|
||||
<style type="text/css" emogrify="no"> @media (max-width: 600px) { .gmx-killpill { content: " \03D1"; } .r0-o { border-style: solid !important; margin: 0 auto 0 auto !important; width: 320px !important; } .r1-i { background-color: #ffffff !important; } .r2-c { box-sizing: border-box !important; text-align: center !important; valign: top !important; width: 100% !important; } .r3-o { border-style: solid !important; margin: 0 auto 0 auto !important; width: 100% !important; } .r4-i { padding-bottom: 20px !important; padding-left: 15px !important; padding-right: 15px !important; padding-top: 20px !important; } .r5-c { box-sizing: border-box !important; display: block !important; valign: top !important; width: 100% !important; } .r6-o { border-style: solid !important; width: 100% !important; } .r7-i { padding-left: 0px !important; padding-right: 0px !important; } .r8-c { box-sizing: border-box !important; text-align: left !important; valign: top !important; width: 100% !important; } .r9-o { border-style: solid !important; margin: 0 auto 0 0 !important; width: 100% !important; } .r10-i { padding-bottom: 15px !important; padding-left: 15px !important; padding-right: 15px !important; padding-top: 15px !important; } .r11-i { padding-bottom: 10px !important; padding-top: 10px !important; text-align: left !important; } .r12-i { padding-bottom: 10px !important; padding-left: 0px !important; padding-right: 0px !important; padding-top: 10px !important; } .r13-o { border-bottom-color: #d9e4ff !important; border-bottom-width: 1px !important; border-left-color: #d9e4ff !important; border-left-width: 1px !important; border-right-color: #d9e4ff !important; border-right-width: 1px !important; border-style: solid !important; border-top-color: #d9e4ff !important; border-top-width: 1px !important; margin: 0 auto 0 0 !important; width: 100% !important; } .r14-i { background-color: #ecf1ff !important; padding-bottom: 10px !important; padding-left: 10px !important; padding-right: 10px !important; padding-top: 10px !important; text-align: left !important; } .r15-o { border-bottom-color: #efefef !important; border-bottom-width: 2px !important; border-left-color: #efefef !important; border-left-width: 2px !important; border-right-color: #efefef !important; border-right-width: 2px !important; border-style: solid !important; border-top-color: #efefef !important; border-top-width: 2px !important; margin: 0 auto 0 0 !important; width: 100% !important; } .r16-i { padding-bottom: 5px !important; padding-left: 5px !important; padding-right: 5px !important; padding-top: 5px !important; text-align: left !important; } .r17-i { padding-bottom: 5px !important; padding-left: 15px !important; padding-right: 15px !important; padding-top: 5px !important; } .r18-c { box-sizing: border-box !important; text-align: center !important; width: 100% !important; } .r19-c { box-sizing: border-box !important; width: 100% !important; } .r20-i { font-size: 0px !important; padding-bottom: 10px !important; padding-left: 65px !important; padding-right: 65px !important; padding-top: 10px !important; } .r21-c { box-sizing: border-box !important; width: 32px !important; } .r22-o { border-style: solid !important; margin-right: 8px !important; width: 32px !important; } .r23-i { padding-bottom: 5px !important; padding-top: 5px !important; } .r24-o { border-style: solid !important; margin-right: 0px !important; width: 32px !important; } .r25-i { padding-bottom: 0px !important; padding-top: 5px !important; text-align: center !important; } body { -webkit-text-size-adjust: none; } .nl2go-responsive-hide { display: none; } .nl2go-body-table { min-width: unset !important; } .mobshow { height: auto !important; overflow: visible !important; max-height: unset !important; visibility: visible !important; border: none !important; } .resp-table { display: inline-table !important; } .magic-resp { display: table-cell !important; } } </style>
|
||||
<style type="text/css"> p, h1, h2, h3, h4, ol, ul { margin: 0; } a, a:link { color: #006399; text-decoration: underline; } .nl2go-default-textstyle { color: #3b3f44; font-family: georgia, serif; font-size: 16px; line-height: 1.5; word-break: break-word; } .default-button { color: #ffffff; font-family: georgia, serif; font-size: 16px; font-style: normal; font-weight: normal; line-height: 1.15; text-decoration: none; word-break: break-word; } .default-heading1 { color: #1f2d3d; font-family: arial, helvetica, sans-serif; font-size: 36px; word-break: break-word; } .default-heading2 { color: #1f2d3d; font-family: arial, helvetica, sans-serif; font-size: 32px; word-break: break-word; } .default-heading3 { color: #1f2d3d; font-family: arial, helvetica, sans-serif; font-size: 24px; word-break: break-word; } .default-heading4 { color: #1f2d3d; font-family: arial, helvetica, sans-serif; font-size: 18px; word-break: break-word; } a[x-apple-data-detectors] { color: inherit !important; text-decoration: inherit !important; font-size: inherit !important; font-family: inherit !important; font-weight: inherit !important; line-height: inherit !important; } .no-show-for-you { border: none; display: none; float: none; font-size: 0; height: 0; line-height: 0; max-height: 0; mso-hide: all; overflow: hidden; table-layout: fixed; visibility: hidden; width: 0; } </style>
|
||||
<!--[if mso ]>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:AllowPNG />
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
<! [endif]-->
|
||||
<style type="text/css"> a:link { color: #006399; text-decoration: underline; } </style>
|
||||
</head>
|
||||
|
||||
<body
|
||||
bgcolor="#f9f9f9"
|
||||
style="
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI',
|
||||
sans-serif;
|
||||
font-weight: 400;
|
||||
color: #474747;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-text-size-adjust: none;
|
||||
background-color: #f9f9f9;
|
||||
"
|
||||
>
|
||||
<!-- Preheader (hidden) -->
|
||||
<span
|
||||
class="preheader"
|
||||
style="
|
||||
display: none;
|
||||
font-size: 1px;
|
||||
color: transparent;
|
||||
line-height: 1px;
|
||||
max-height: 0;
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
mso-hide: all;
|
||||
visibility: hidden;
|
||||
"
|
||||
>
|
||||
Your Plane login code {{code}} is valid for 10 minutes.
|
||||
</span>
|
||||
|
||||
<!-- Outer Wrapper -->
|
||||
<center
|
||||
class="email-body"
|
||||
style="width: 100%; background-color: #f9f9f9; padding: 40px 0;"
|
||||
>
|
||||
<!-- HEADER -->
|
||||
<table role="presentation" width="100%">
|
||||
<tr>
|
||||
<td></td>
|
||||
|
||||
<td
|
||||
class="email-container"
|
||||
width="620"
|
||||
style="width: 620px; max-width: 620px; margin: 0 auto;"
|
||||
>
|
||||
<table
|
||||
role="presentation"
|
||||
width="100%"
|
||||
style="background-color: #000000;"
|
||||
>
|
||||
<tr>
|
||||
<td style="padding: 24px 40px; text-align: center;">
|
||||
<img
|
||||
src="https://media.docs.plane.so/logo/new-logo-dark.png"
|
||||
width="120"
|
||||
alt="Plane"
|
||||
style="height: auto; display: block; margin: 0 auto;"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
<td></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- BODY (card under header) -->
|
||||
<table role="presentation" width="100%">
|
||||
<tr>
|
||||
<td></td>
|
||||
|
||||
<td
|
||||
class="email-container"
|
||||
width="620"
|
||||
style="width: 620px; max-width: 620px; margin: 0 auto;"
|
||||
>
|
||||
<table
|
||||
role="presentation"
|
||||
width="100%"
|
||||
class="email-content"
|
||||
style="background-color: #ffffff; padding: 32px 40px 32px 40px;"
|
||||
>
|
||||
<tr>
|
||||
<td
|
||||
style="
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
color: #474747;
|
||||
padding: 24px;
|
||||
"
|
||||
>
|
||||
<!-- ========================================= -->
|
||||
<!-- PLANE LOGIN CODE EMAIL -->
|
||||
<!-- ========================================= -->
|
||||
|
||||
<!-- Heading -->
|
||||
<h1
|
||||
style="
|
||||
margin: 0 0 12px;
|
||||
font-size: 22px;
|
||||
line-height: 1.35;
|
||||
font-weight: 600;
|
||||
color: #111111;
|
||||
"
|
||||
>
|
||||
Your login code for Plane
|
||||
</h1>
|
||||
|
||||
<!-- Code chip -->
|
||||
<p style="margin: 12px 0 24px;">
|
||||
<span
|
||||
style="
|
||||
display: inline-block;
|
||||
padding: 8px 14px;
|
||||
background-color: #e4e5e7;
|
||||
border-radius: 6px;
|
||||
font-family: 'SF Mono', Menlo, Monaco, Consolas,
|
||||
'Liberation Mono', 'Courier New', monospace;
|
||||
font-size: 15px;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
color: #111111;
|
||||
"
|
||||
>
|
||||
{{code}}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<!-- Main info -->
|
||||
<p
|
||||
style="
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: #5f5e5e;
|
||||
"
|
||||
>
|
||||
This code is valid for the next 10 minutes. Enter it on the
|
||||
sign-in page to complete your login.
|
||||
</p>
|
||||
|
||||
<!-- Secondary info -->
|
||||
<p
|
||||
style="
|
||||
margin: 16px 0 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: #5f5e5e;
|
||||
"
|
||||
>
|
||||
Note: This email was sent to
|
||||
<span
|
||||
style="
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 12px;
|
||||
background-color: #f3f3f3;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #e9e9e9;
|
||||
"
|
||||
>{{email}}</span
|
||||
>. If you were not expecting a sign-in code, you can safely
|
||||
ignore this message.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
<td></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- FOOTER (simple, aligned with body) -->
|
||||
<table role="presentation" width="100%">
|
||||
<tr>
|
||||
<td></td>
|
||||
|
||||
<td
|
||||
class="email-container"
|
||||
width="620"
|
||||
style="width: 620px; max-width: 620px; margin: 0 auto;"
|
||||
>
|
||||
<table
|
||||
role="presentation"
|
||||
width="100%"
|
||||
class="email-footer"
|
||||
style="background-color: #ffffff; padding: 0 40px 16px 40px;"
|
||||
>
|
||||
<tr>
|
||||
<td
|
||||
style="
|
||||
border-top: 1px solid #e9e9e9;
|
||||
padding: 24px;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
color: #979797;
|
||||
text-align: left;
|
||||
"
|
||||
>
|
||||
<p style="margin: 0;">
|
||||
Plane Software, Inc.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
<td></td>
|
||||
</tr>
|
||||
</table>
|
||||
</center>
|
||||
|
||||
<base target="_blank" />
|
||||
<body bgcolor="#ffffff" text="#3b3f44" link="#006399" yahoo="fix" style="background-color: #ffffff" >
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" class="nl2go-body-table" width="100%" style="background-color: #ffffff; width: 100%" >
|
||||
<tr>
|
||||
<td>
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="600" align="center" class="r0-o" style="table-layout: fixed; width: 600px" >
|
||||
<tr>
|
||||
<td valign="top" class="r1-i" style="background-color: #ffffff">
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" align="center" class="r3-o" style="table-layout: fixed; width: 100%" >
|
||||
<tr>
|
||||
<td class="r4-i" style="padding-bottom: 20px; padding-top: 20px" >
|
||||
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
|
||||
<tr>
|
||||
<th width="100%" valign="top" class="r5-c" style="font-weight: normal" >
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r6-o" style="table-layout: fixed; width: 100%" >
|
||||
<tr>
|
||||
<td valign="top" class="r7-i" style=" padding-left: 10px; padding-right: 10px; " >
|
||||
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
|
||||
<tr>
|
||||
<td class="r8-c" align="left">
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="150" class="r9-o" style=" table-layout: fixed; width: 150px; " >
|
||||
<tr>
|
||||
<td style=" font-size: 0px; line-height: 0px; " > <img src="https://media.docs.plane.so/logo/new-logo-white.png" width="150" border="0" style=" display: block; width: 100%; " /> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</th>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" align="center" class="r3-o" style="table-layout: fixed; width: 100%" >
|
||||
<tr>
|
||||
<td class="r10-i" style="padding-bottom: 15px; padding-top: 15px" >
|
||||
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
|
||||
<tr>
|
||||
<th width="100%" valign="top" class="r5-c" style="font-weight: normal" >
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r6-o" style="table-layout: fixed; width: 100%" >
|
||||
<tr>
|
||||
<td valign="top" class="r7-i" style=" padding-left: 15px; padding-right: 15px; " >
|
||||
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
|
||||
<tr>
|
||||
<td class="r8-c" align="left">
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r9-o" style=" table-layout: fixed; width: 100%; " >
|
||||
<tr>
|
||||
<td align="left" valign="top" class="r11-i nl2go-default-textstyle" style=" color: #3b3f44; font-family: georgia, serif; font-size: 16px; line-height: 1.5; word-break: break-word; padding-bottom: 10px; padding-top: 10px; text-align: left; " >
|
||||
<div>
|
||||
<h2 class="default-heading2" style=" margin: 0; color: #1f2d3d; font-family: arial, helvetica, sans-serif; font-size: 32px; word-break: break-word; " > The code below is only valid for 10 minutes. </h2>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</th>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" align="center" class="r3-o" style="table-layout: fixed; width: 100%" >
|
||||
<tr>
|
||||
<td class="r12-i" style="padding-bottom: 10px; padding-top: 10px" >
|
||||
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
|
||||
<tr>
|
||||
<th width="100%" valign="top" class="r5-c" style="font-weight: normal" >
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r6-o" style="table-layout: fixed; width: 100%" >
|
||||
<tr>
|
||||
<td valign="top" class="r7-i" style=" padding-left: 15px; padding-right: 15px; " >
|
||||
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
|
||||
<tr>
|
||||
<td class="r8-c" align="left">
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r13-o" style=" background-color: #ecf1ff; border-bottom-color: #d9e4ff; border-bottom-width: 1px; border-collapse: separate; border-left-color: #d9e4ff; border-left-width: 1px; border-radius: 5px; border-right-color: #d9e4ff; border-right-width: 1px; border-style: solid; border-top-color: #d9e4ff; border-top-width: 1px; table-layout: fixed; width: 100%; " >
|
||||
<tr>
|
||||
<td align="left" valign="top" class="r14-i nl2go-default-textstyle" style=" color: #3b3f44; font-family: georgia, serif; font-size: 16px; word-break: break-word; background-color: #ecf1ff; border-radius: 4px; line-height: 3; padding-bottom: 10px; padding-left: 10px; padding-right: 10px; padding-top: 10px; text-align: left; " >
|
||||
<div>
|
||||
<p style="margin: 0"> <span style=" font-family: Arial, helvetica, sans-serif; font-size: 24px; " >{{code}}</span > </p>
|
||||
<p style="margin: 0"> <span style=" color: #5f5e5e; font-family: Arial, helvetica, sans-serif; font-size: 14px; " >Please copy and paste this on the screen where you requested this from.</span > </p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</th>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" align="center" class="r3-o" style="table-layout: fixed; width: 100%" >
|
||||
<tr>
|
||||
<td class="r10-i" style="padding-bottom: 15px; padding-top: 15px" >
|
||||
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
|
||||
<tr>
|
||||
<th width="100%" valign="top" class="r5-c" style="font-weight: normal" >
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r6-o" style="table-layout: fixed; width: 100%" >
|
||||
<tr>
|
||||
<td valign="top" class="r7-i" style=" padding-left: 15px; padding-right: 15px; " >
|
||||
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
|
||||
<tr>
|
||||
<td class="r8-c" align="left">
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r15-o" style=" table-layout: fixed; width: 100%; " >
|
||||
<tr class="nl2go-responsive-hide">
|
||||
<td height="2" width="2" style=" font-size: 2px; line-height: 2px; background-color: #efefef; " > </td>
|
||||
<td height="2" style=" font-size: 2px; line-height: 2px; background-color: #efefef; " > </td>
|
||||
<td height="2" width="2" style=" font-size: 2px; line-height: 2px; background-color: #efefef; " > </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="nl2go-responsive-hide" width="2" style=" font-size: 0px; line-height: 1px; background-color: #efefef; " > </td>
|
||||
<td align="left" valign="top" class="r16-i nl2go-default-textstyle" style=" color: #3b3f44; font-family: georgia, serif; font-size: 16px; word-break: break-word; line-height: 1.5; padding-bottom: 5px; padding-left: 5px; padding-right: 5px; padding-top: 5px; text-align: left; " >
|
||||
<div>
|
||||
<p style="margin: 0"> <span style="font-size: 13px" >Despite our popularity, we are humbly early-stage. We are shipping fast, so please reach out to us with feature requests, major and minor nits, and anything else you find missing. We read every </span ><a href="https://discord.com/channels/1031547764020084846/1094927053867995176" title="Plane Support on Discod" target="_blank" style=" color: #006399; text-decoration: underline; " ><span style="font-size: 13px" >message</span ></a ><span style="font-size: 13px" >, </span ><a href="http://twitter.com/planepowers" title="@planepowers" target="_blank" style=" color: #006399; text-decoration: underline; " ><span style="font-size: 13px" >tweet</span ></a ><span style="font-size: 13px" >, and </span ><a href="https://github.com/makeplane/plane/issues" title="Plane's GitHub conversations" target="_blank" style=" color: #006399; text-decoration: underline; " ><span style="font-size: 13px" >conversation</span ></a ><span style="font-size: 13px" > and update </span ><a href="https://plane.sh/plane/0b170a1c-0e55-47cb-9307-ea49a05672b5?board=kanban" title="Plane's roadmap" target="_blank" style=" color: #006399; text-decoration: underline; " ><span style="font-size: 13px" >our public roadmap</span ></a ><span style="font-size: 13px" >.</span > </p>
|
||||
</div>
|
||||
</td>
|
||||
<td class="nl2go-responsive-hide" width="2" style=" font-size: 0px; line-height: 1px; background-color: #efefef; " > </td>
|
||||
</tr>
|
||||
<tr class="nl2go-responsive-hide">
|
||||
<td height="2" width="2" style=" font-size: 2px; line-height: 2px; background-color: #efefef; " > </td>
|
||||
<td height="2" style=" font-size: 2px; line-height: 2px; background-color: #efefef; " > </td>
|
||||
<td height="2" width="2" style=" font-size: 2px; line-height: 2px; background-color: #efefef; " > </td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</th>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" align="center" class="r3-o" style="table-layout: fixed; width: 100%" >
|
||||
<tr>
|
||||
<td class="r17-i" style="padding-bottom: 5px; padding-top: 5px" >
|
||||
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
|
||||
<tr>
|
||||
<th width="100%" valign="top" class="r5-c" style="font-weight: normal" >
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r6-o" style="table-layout: fixed; width: 100%" >
|
||||
<tr>
|
||||
<td valign="top" class="r7-i" style=" padding-left: 15px; padding-right: 15px; " >
|
||||
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
|
||||
<tr>
|
||||
<td class="r18-c" align="center">
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="570" align="center" class="r3-o" style=" table-layout: fixed; width: 570px; " >
|
||||
<tr>
|
||||
<td valign="top">
|
||||
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
|
||||
<tr>
|
||||
<td class="r19-c" style=" display: inline-block; " >
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="570" class="r6-o" style=" table-layout: fixed; width: 570px; " >
|
||||
<tr>
|
||||
<td class="r20-i" style=" padding-bottom: 10px; padding-left: 209px; padding-right: 209px; padding-top: 10px; " >
|
||||
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
|
||||
<tr>
|
||||
<th width="40" class="r21-c mobshow resp-table" style=" font-weight: normal; " >
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r22-o" style=" table-layout: fixed; width: 100%; " >
|
||||
<tr>
|
||||
<td class="r23-i" style=" font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px; " > <a href="https://github.com/makeplane" target="_blank" style=" color: #006399; text-decoration: underline; " > <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/github_32px.png" width="32" border="0" style=" display: block; width: 100%; " /></a> </td>
|
||||
<td class="nl2go-responsive-hide" width="8" style=" font-size: 0px; line-height: 1px; " > </td>
|
||||
</tr>
|
||||
</table>
|
||||
</th>
|
||||
<th width="40" class="r21-c mobshow resp-table" style=" font-weight: normal; " >
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r22-o" style=" table-layout: fixed; width: 100%; " >
|
||||
<tr>
|
||||
<td class="r23-i" style=" font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px; " > <a href="https://www.linkedin.com/company/planepowers/" target="_blank" style=" color: #006399; text-decoration: underline; " > <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/linkedin_32px.png" width="32" border="0" style=" display: block; width: 100%; " /></a> </td>
|
||||
<td class="nl2go-responsive-hide" width="8" style=" font-size: 0px; line-height: 1px; " > </td>
|
||||
</tr>
|
||||
</table>
|
||||
</th>
|
||||
<th width="40" class="r21-c mobshow resp-table" style=" font-weight: normal; " >
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r22-o" style=" table-layout: fixed; width: 100%; " >
|
||||
<tr>
|
||||
<td class="r23-i" style=" font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px; " > <a href="https://twitter.com/planepowers" target="_blank" style=" color: #006399; text-decoration: underline; " > <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/twitter_32px.png" width="32" border="0" style=" display: block; width: 100%; " /></a> </td>
|
||||
<td class="nl2go-responsive-hide" width="8" style=" font-size: 0px; line-height: 1px; " > </td>
|
||||
</tr>
|
||||
</table>
|
||||
</th>
|
||||
<th width="32" class="r21-c mobshow resp-table" style=" font-weight: normal; " >
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r24-o" style=" table-layout: fixed; width: 100%; " >
|
||||
<tr>
|
||||
<td class="r23-i" style=" font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px; " > <a href="https://plane.so/" target="_blank" style=" color: #006399; text-decoration: underline; " > <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/website_32px.png" width="32" border="0" style=" display: block; width: 100%; " /></a> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</th>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</th>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" align="center" class="r3-o" style="table-layout: fixed; width: 100%" >
|
||||
<tr>
|
||||
<td class="r17-i" style="padding-bottom: 5px; padding-top: 5px" >
|
||||
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
|
||||
<tr>
|
||||
<th width="100%" valign="top" class="r5-c" style="font-weight: normal" >
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r6-o" style="table-layout: fixed; width: 100%" >
|
||||
<tr>
|
||||
<td valign="top" class="r7-i" style=" padding-left: 15px; padding-right: 15px; " >
|
||||
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
|
||||
<tr>
|
||||
<td class="r8-c" align="left">
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r9-o" style=" table-layout: fixed; width: 100%; " >
|
||||
<tr>
|
||||
<td align="center" valign="top" class="r25-i nl2go-default-textstyle" style=" color: #3b3f44; font-family: georgia, serif; word-break: break-word; font-size: 18px; line-height: 1.5; padding-top: 5px; text-align: center; " >
|
||||
<div>
|
||||
<p style=" margin: 0; font-size: 14px; " > <span style="font-size: 12px" >This email was sent to {{email}}. Please delete if you aren't the intended recipient.</span > </p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</th>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,215 +1,2 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Your Plane Analytics export is ready</title>
|
||||
|
||||
<style>
|
||||
/* Basic resets for email clients */
|
||||
body,
|
||||
table,
|
||||
td,
|
||||
p {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
line-height: 100%;
|
||||
text-decoration: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
/* Mobile adjustments */
|
||||
@media only screen and (max-width: 600px) {
|
||||
.email-container {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.email-content {
|
||||
padding: 24px 20px !important;
|
||||
}
|
||||
|
||||
.email-footer {
|
||||
padding: 0 20px 16px 20px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body
|
||||
bgcolor="#f9f9f9"
|
||||
style="
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI',
|
||||
sans-serif;
|
||||
font-weight: 400;
|
||||
color: #474747;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-text-size-adjust: none;
|
||||
background-color: #f9f9f9;
|
||||
"
|
||||
>
|
||||
<!-- Preheader (hidden) -->
|
||||
<span
|
||||
class="preheader"
|
||||
style="
|
||||
display: none;
|
||||
font-size: 1px;
|
||||
color: transparent;
|
||||
line-height: 1px;
|
||||
max-height: 0;
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
mso-hide: all;
|
||||
visibility: hidden;
|
||||
"
|
||||
>
|
||||
Your requested Plane Analytics data export is attached as a CSV file.
|
||||
</span>
|
||||
|
||||
<!-- Outer Wrapper -->
|
||||
<center
|
||||
class="email-body"
|
||||
style="width: 100%; background-color: #f9f9f9; padding: 40px 0;"
|
||||
>
|
||||
<!-- HEADER -->
|
||||
<table role="presentation" width="100%">
|
||||
<tr>
|
||||
<td></td>
|
||||
<td
|
||||
class="email-container"
|
||||
width="620"
|
||||
style="width: 620px; max-width: 620px; margin: 0 auto;"
|
||||
>
|
||||
<table role="presentation" width="100%" style="background-color: #000000;">
|
||||
<tr>
|
||||
<td style="padding: 24px 40px; text-align: center;">
|
||||
<img
|
||||
src="https://media.docs.plane.so/logo/new-logo-dark.png"
|
||||
width="120"
|
||||
alt="Plane"
|
||||
style="height: auto; display: block; margin: 0 auto;"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- BODY (card under header) -->
|
||||
<table role="presentation" width="100%">
|
||||
<tr>
|
||||
<td></td>
|
||||
<td
|
||||
class="email-container"
|
||||
width="620"
|
||||
style="width: 620px; max-width: 620px; margin: 0 auto;"
|
||||
>
|
||||
<table
|
||||
role="presentation"
|
||||
width="100%"
|
||||
class="email-content"
|
||||
style="background-color: #ffffff; padding: 32px 40px 32px 40px;"
|
||||
>
|
||||
<tr>
|
||||
<td
|
||||
style="
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
color: #474747;
|
||||
padding: 24px;
|
||||
"
|
||||
>
|
||||
<!-- ========================================= -->
|
||||
<!-- PLANE ANALYTICS DATA EXPORT EMAIL -->
|
||||
<!-- ========================================= -->
|
||||
|
||||
<!-- Heading -->
|
||||
<h1
|
||||
style="
|
||||
margin: 0 0 12px;
|
||||
font-size: 22px;
|
||||
line-height: 1.35;
|
||||
font-weight: 600;
|
||||
color: #111111;
|
||||
"
|
||||
>
|
||||
Your Plane Analytics export is ready
|
||||
</h1>
|
||||
|
||||
<!-- Body copy -->
|
||||
<p style="margin: 0 0 16px;">
|
||||
Your requested data export from Plane Analytics is ready. We’ve attached it as a CSV file for your convenience.
|
||||
</p>
|
||||
|
||||
<!-- Small-note styled paragraphs -->
|
||||
<p
|
||||
style="
|
||||
margin: 0 0 12px;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: #5f5e5e;
|
||||
"
|
||||
>
|
||||
You can open the CSV in any spreadsheet or BI tool to analyze the data, build reports, or share insights with your team.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<table role="presentation" width="100%">
|
||||
<tr>
|
||||
<td></td>
|
||||
<td
|
||||
class="email-container"
|
||||
width="620"
|
||||
style="width: 620px; max-width: 620px; margin: 0 auto;"
|
||||
>
|
||||
<table
|
||||
role="presentation"
|
||||
width="100%"
|
||||
class="email-footer"
|
||||
style="background-color: #ffffff; padding: 0 40px 16px 40px;"
|
||||
>
|
||||
<tr>
|
||||
<td
|
||||
style="
|
||||
border-top: 1px solid #e9e9e9;
|
||||
padding: 24px;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
color: #979797;
|
||||
text-align: left;
|
||||
"
|
||||
>
|
||||
<p style="margin: 0;">
|
||||
Plane Software, Inc.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</table>
|
||||
</center>
|
||||
|
||||
<base target="_blank" />
|
||||
</body>
|
||||
</html>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html> Hey there,<br/> Your requested data export from Plane Analytics is now ready. The information has been compiled into a CSV format for your convenience.<br/> Please find the attachment and download the CSV file. This file can easily be imported into any spreadsheet program for further analysis.<br/> If you require any assistance or have any questions, please do not hesitate to contact us.<br/> Thank you </html>
|
||||
@@ -1,306 +1,219 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>{{first_name}} invited you to {{workspace_name}} on Plane</title>
|
||||
|
||||
<style>
|
||||
/* Basic resets for email clients */
|
||||
body,
|
||||
table,
|
||||
td,
|
||||
p {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
line-height: 100%;
|
||||
text-decoration: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
/* Mobile adjustments */
|
||||
@media only screen and (max-width: 600px) {
|
||||
.email-container {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.email-content {
|
||||
padding: 24px 20px !important;
|
||||
}
|
||||
|
||||
.email-footer {
|
||||
padding: 0 20px 16px 20px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{first_name}} has invited you to join them in {{workspace_name}} on Plane.</title>
|
||||
<style type="text/css" emogrify="no">#outlook a { padding:0; } .ExternalClass { width:100%; } .ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div { line-height: 100%; } table td { border-collapse: collapse; mso-line-height-rule: exactly; } .editable.image { font-size: 0 !important; line-height: 0 !important; } .nl2go_preheader { display: none !important; mso-hide:all !important; mso-line-height-rule: exactly; visibility: hidden !important; line-height: 0px !important; font-size: 0px !important; } body { width:100% !important; -webkit-text-size-adjust:100%; -ms-text-size-adjust:100%; margin:0; padding:0; } img { outline:none; text-decoration:none; -ms-interpolation-mode: bicubic; } a img { border:none; } table { border-collapse:collapse; mso-table-lspace:0pt; mso-table-rspace:0pt; } th { font-weight: normal; text-align: left; } *[class="gmail-fix"] { display: none !important; } </style>
|
||||
<style type="text/css" emogrify="no"> @media (max-width: 600px) { .gmx-killpill { content: ' \03D1';} } </style>
|
||||
<style type="text/css" emogrify="no">@media (max-width: 600px) { .gmx-killpill { content: ' \03D1';} .r0-o { border-style: solid !important; margin: 0 auto 0 auto !important; width: 320px !important } .r1-i { background-color: #ffffff !important } .r2-c { box-sizing: border-box !important; text-align: center !important; valign: top !important; width: 100% !important } .r3-o { border-style: solid !important; margin: 0 auto 0 auto !important; width: 100% !important } .r4-i { padding-bottom: 20px !important; padding-left: 15px !important; padding-right: 15px !important; padding-top: 20px !important } .r5-c { box-sizing: border-box !important; display: block !important; valign: top !important; width: 100% !important } .r6-o { border-style: solid !important; width: 100% !important } .r7-i { padding-left: 0px !important; padding-right: 0px !important } .r8-i { padding-bottom: 20px !important; padding-left: 10px !important; padding-right: 10px !important; padding-top: 20px !important } .r9-c { box-sizing: border-box !important; text-align: left !important; valign: top !important; width: 100% !important } .r10-o { border-style: solid !important; margin: 0 auto 0 0 !important; width: 100% !important } .r11-i { padding-top: 15px !important; text-align: center !important } .r12-c { box-sizing: border-box !important; padding-bottom: 15px !important; padding-top: 15px !important; text-align: left !important; valign: top !important; width: 100% !important } .r13-c { box-sizing: border-box !important; padding: 0 !important; text-align: center !important; valign: top !important; width: 100% !important } .r14-o { border-style: solid !important; margin: 0 auto 0 auto !important; margin-bottom: 15px !important; margin-top: 15px !important; width: 100% !important } .r15-i { padding: 0 !important; text-align: center !important } .r16-r { background-color: #006399 !important; border-radius: 4px !important; border-width: 0px !important; box-sizing: border-box; height: initial !important; padding: 0 !important; padding-bottom: 12px !important; padding-left: 5px !important; padding-right: 5px !important; padding-top: 12px !important; text-align: center !important; width: 100% !important } .r17-c { box-sizing: border-box !important; text-align: center !important; width: 100% !important } .r18-c { box-sizing: border-box !important; width: 100% !important } .r19-i { font-size: 0px !important; padding-bottom: 15px !important; padding-left: 65px !important; padding-right: 65px !important; padding-top: 15px !important } .r20-c { box-sizing: border-box !important; width: 32px !important } .r21-o { border-style: solid !important; margin-right: 8px !important; width: 32px !important } .r22-i { padding-bottom: 5px !important; padding-top: 5px !important } .r23-o { border-style: solid !important; margin-right: 0px !important; width: 32px !important } body { -webkit-text-size-adjust: none } .nl2go-responsive-hide { display: none } .nl2go-body-table { min-width: unset !important } .mobshow { height: auto !important; overflow: visible !important; max-height: unset !important; visibility: visible !important; border: none !important } .resp-table { display: inline-table !important } .magic-resp { display: table-cell !important } } </style>
|
||||
<style type="text/css">p, h1, h2, h3, h4, ol, ul, li { margin: 0; } a, a:link { color: #006399; text-decoration: underline } .nl2go-default-textstyle { color: #3b3f44; font-family: georgia, serif; font-size: 18px; line-height: 1.5; word-break: break-word } .default-button { color: #ffffff; font-family: georgia, serif; font-size: 16px; font-style: normal; font-weight: bold; line-height: 1.15; text-decoration: none; word-break: break-word } .default-heading1 { color: #1F2D3D; font-family: arial,helvetica,sans-serif; font-size: 36px; word-break: break-word } .default-heading2 { color: #1F2D3D; font-family: arial,helvetica,sans-serif; font-size: 32px; word-break: break-word } .default-heading3 { color: #1F2D3D; font-family: arial,helvetica,sans-serif; font-size: 24px; word-break: break-word } .default-heading4 { color: #1F2D3D; font-family: arial,helvetica,sans-serif; font-size: 18px; word-break: break-word } a[x-apple-data-detectors] { color: inherit !important; text-decoration: inherit !important; font-size: inherit !important; font-family: inherit !important; font-weight: inherit !important; line-height: inherit !important; } .no-show-for-you { border: none; display: none; float: none; font-size: 0; height: 0; line-height: 0; max-height: 0; mso-hide: all; overflow: hidden; table-layout: fixed; visibility: hidden; width: 0; } </style>
|
||||
<!--[if mso]>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:AllowPNG/>
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
<![endif]-->
|
||||
<style type="text/css">a:link{color: #006399; text-decoration: underline;}</style>
|
||||
</head>
|
||||
|
||||
<body
|
||||
bgcolor="#f9f9f9"
|
||||
style="
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI',
|
||||
sans-serif;
|
||||
font-weight: 400;
|
||||
color: #474747;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-text-size-adjust: none;
|
||||
background-color: #f9f9f9;
|
||||
"
|
||||
>
|
||||
<!-- Preheader (hidden) -->
|
||||
<span
|
||||
class="preheader"
|
||||
style="
|
||||
display: none;
|
||||
font-size: 1px;
|
||||
color: transparent;
|
||||
line-height: 1px;
|
||||
max-height: 0;
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
mso-hide: all;
|
||||
visibility: hidden;
|
||||
"
|
||||
>
|
||||
{{first_name}} has invited you to join {{workspace_name}} on Plane.
|
||||
</span>
|
||||
|
||||
<!-- Outer Wrapper -->
|
||||
<center
|
||||
class="email-body"
|
||||
style="width: 100%; background-color: #f9f9f9; padding: 40px 0;"
|
||||
>
|
||||
<!-- HEADER -->
|
||||
<table role="presentation" width="100%">
|
||||
<tr>
|
||||
<td></td>
|
||||
|
||||
<td
|
||||
class="email-container"
|
||||
width="620"
|
||||
style="width: 620px; max-width: 620px; margin: 0 auto;"
|
||||
>
|
||||
<table
|
||||
role="presentation"
|
||||
width="100%"
|
||||
style="background-color: #000000;"
|
||||
>
|
||||
<tr>
|
||||
<td style="padding: 24px 40px; text-align: center;">
|
||||
<img
|
||||
src="https://media.docs.plane.so/logo/new-logo-dark.png"
|
||||
width="120"
|
||||
alt="Plane"
|
||||
style="height: auto; display: block; margin: 0 auto;"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
<td></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- BODY (card under header) -->
|
||||
<table role="presentation" width="100%">
|
||||
<tr>
|
||||
<td></td>
|
||||
|
||||
<td
|
||||
class="email-container"
|
||||
width="620"
|
||||
style="width: 620px; max-width: 620px; margin: 0 auto;"
|
||||
>
|
||||
<table
|
||||
role="presentation"
|
||||
width="100%"
|
||||
class="email-content"
|
||||
style="background-color: #ffffff; padding: 32px 40px 32px 40px;"
|
||||
>
|
||||
<tr>
|
||||
<td
|
||||
style="
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
color: #474747;
|
||||
padding: 24px;
|
||||
"
|
||||
>
|
||||
<!-- ========================================= -->
|
||||
<!-- PLANE WORKSPACE INVITE EMAIL -->
|
||||
<!-- ========================================= -->
|
||||
|
||||
<!-- Heading -->
|
||||
<h1
|
||||
style="
|
||||
margin: 0 0 12px;
|
||||
font-size: 22px;
|
||||
line-height: 1.35;
|
||||
font-weight: 600;
|
||||
color: #111111;
|
||||
"
|
||||
>
|
||||
Join {{workspace_name}} on Plane
|
||||
</h1>
|
||||
|
||||
<!-- Intro copy -->
|
||||
<p style="margin: 0 0 12px;">
|
||||
{{first_name}} has invited you to collaborate with them in
|
||||
<strong>{{workspace_name}}</strong> workspace on Plane.
|
||||
</p>
|
||||
|
||||
<!-- Primary button -->
|
||||
<table
|
||||
role="presentation"
|
||||
cellspacing="0"
|
||||
cellpadding="0"
|
||||
border="0"
|
||||
style="margin: 0 0 24px;"
|
||||
>
|
||||
<tr>
|
||||
<td>
|
||||
<a
|
||||
href="{{abs_url}}"
|
||||
target="_blank"
|
||||
style="
|
||||
background-color: #1080bc;
|
||||
border-radius: 6px;
|
||||
color: #ffffff;
|
||||
padding: 12px 24px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
"
|
||||
>
|
||||
Accept invite
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Secondary info -->
|
||||
<p
|
||||
style="
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: #5f5e5e;
|
||||
"
|
||||
>
|
||||
If the button doesn’t work, copy and paste this link into
|
||||
your browser:
|
||||
</p>
|
||||
|
||||
<p
|
||||
style="
|
||||
margin: 6px 0 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: #5f5e5e;
|
||||
"
|
||||
>
|
||||
<span
|
||||
style="
|
||||
display: inline-block;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 12px;
|
||||
background-color: #f3f3f3;
|
||||
padding: 4px 6px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #e9e9e9;
|
||||
word-break: break-all;
|
||||
"
|
||||
>{{abs_url}}</span
|
||||
>
|
||||
</p>
|
||||
|
||||
<!-- Note -->
|
||||
<p
|
||||
style="
|
||||
margin: 16px 0 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: #5f5e5e;
|
||||
"
|
||||
>
|
||||
Note: This invitation was intended for
|
||||
<span
|
||||
style="
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 12px;
|
||||
background-color: #f3f3f3;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #e9e9e9;
|
||||
"
|
||||
>{{email}}</span
|
||||
>. If you were not expecting this invitation, you can ignore
|
||||
this email.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
<td></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- FOOTER (simple, aligned with body) -->
|
||||
<table role="presentation" width="100%">
|
||||
<tr>
|
||||
<td></td>
|
||||
|
||||
<td
|
||||
class="email-container"
|
||||
width="620"
|
||||
style="width: 620px; max-width: 620px; margin: 0 auto;"
|
||||
>
|
||||
<table
|
||||
role="presentation"
|
||||
width="100%"
|
||||
class="email-footer"
|
||||
style="background-color: #ffffff; padding: 0 40px 16px 40px;"
|
||||
>
|
||||
<tr>
|
||||
<td
|
||||
style="
|
||||
border-top: 1px solid #e9e9e9;
|
||||
padding: 24px;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
color: #979797;
|
||||
text-align: left;
|
||||
"
|
||||
>
|
||||
<p style="margin: 0;">
|
||||
Plane Software, Inc.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
<td></td>
|
||||
</tr>
|
||||
</table>
|
||||
</center>
|
||||
|
||||
<base target="_blank" />
|
||||
<body bgcolor="#ffffff" text="#3b3f44" link="#006399" yahoo="fix" style="background-color: #ffffff;">
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" class="nl2go-body-table" width="100%" style="background-color: #ffffff; width: 100%;">
|
||||
<tr>
|
||||
<td>
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="600" align="center" class="r0-o" style="table-layout: fixed; width: 600px;">
|
||||
<tr>
|
||||
<td valign="top" class="r1-i" style="background-color: #ffffff;">
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" align="center" class="r3-o" style="table-layout: fixed; width: 100%;">
|
||||
<tr>
|
||||
<td class="r4-i" style="padding-bottom: 20px; padding-top: 20px;">
|
||||
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation">
|
||||
<tr>
|
||||
<th width="100%" valign="top" class="r5-c" style="font-weight: normal;">
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r6-o" style="table-layout: fixed; width: 100%;">
|
||||
<tr>
|
||||
<td valign="top" class="r7-i" style="padding-left: 10px; padding-right: 10px;">
|
||||
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation">
|
||||
<tr>
|
||||
<td class="r2-c" align="center">
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="200" class="r3-o" style="table-layout: fixed; width: 200px;">
|
||||
<tr>
|
||||
<td style="font-size: 0px; line-height: 0px;"> <img src="https://media.docs.plane.so/logo/new-logo-white.png" width="200" border="0" style="display: block; width: 100%;"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</th>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" align="center" class="r3-o" style="table-layout: fixed; width: 100%;">
|
||||
<tr>
|
||||
<td class="r8-i" style="padding-bottom: 20px; padding-top: 20px;">
|
||||
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation">
|
||||
<tr>
|
||||
<th width="100%" valign="top" class="r5-c" style="font-weight: normal;">
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r6-o" style="table-layout: fixed; width: 100%;">
|
||||
<tr>
|
||||
<td valign="top" class="r7-i">
|
||||
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation">
|
||||
<tr>
|
||||
<td class="r9-c" align="left">
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r10-o" style="table-layout: fixed; width: 100%;">
|
||||
<tr>
|
||||
<td align="center" valign="top" class="r11-i nl2go-default-textstyle" style="color: #3b3f44; font-family: georgia,serif; font-size: 18px; word-break: break-word; line-height: 1.5; padding-top: 15px; text-align: center;">
|
||||
<div>
|
||||
<h1 class="default-heading1" style="margin: 0; color: #1f2d3d; font-family: arial,helvetica,sans-serif; font-size: 36px; word-break: break-word;">You are a celebrated colleague!</h1>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="r12-c nl2go-default-textstyle" align="left" style="color: #3b3f44; font-family: georgia,serif; font-size: 18px; line-height: 1.5; word-break: break-word; padding-bottom: 15px; padding-top: 15px; text-align: left; valign: top;">
|
||||
<div>
|
||||
<p style="margin: 0;">{{first_name}} has invited you to join them in {{workspace_name}}, a workspace for projects and progress, on Plane. Some of our users have told us it's a privilege, but we will let you be the judge of that.</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="r13-c" align="center" style="align: center; padding-bottom: 15px; padding-top: 15px; valign: top;">
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="300" class="r14-o" style="background-color: #006399; border-collapse: separate; border-color: #006399; border-radius: 4px; border-style: solid; border-width: 0px; table-layout: fixed; width: 300px;">
|
||||
<tr>
|
||||
<td height="18" align="center" valign="top" class="r15-i nl2go-default-textstyle" style="word-break: break-word; background-color: #006399; border-radius: 4px; color: #ffffff; font-family: georgia, serif; font-size: 16px; font-style: normal; line-height: 1.15; padding-bottom: 12px; padding-left: 5px; padding-right: 5px; padding-top: 12px; text-align: center;"> <a href="{{abs_url}}" class="r16-r default-button" target="_blank" data-btn="1" style="font-style: normal; font-weight: bold; line-height: 1.15; text-decoration: none; word-break: break-word; word-wrap: break-word; display: block; -webkit-text-size-adjust: none; color: #ffffff; font-family: georgia, serif; font-size: 16px;"> <span><span style="font-family: Arial, helvetica, sans-serif;">Join them on Plane</span></span></a> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</th>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" align="center" class="r3-o" style="table-layout: fixed; width: 100%;">
|
||||
<tr>
|
||||
<td class="r4-i" style="padding-bottom: 20px; padding-top: 20px;">
|
||||
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation">
|
||||
<tr>
|
||||
<th width="100%" valign="top" class="r5-c" style="font-weight: normal;">
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r6-o" style="table-layout: fixed; width: 100%;">
|
||||
<tr>
|
||||
<td valign="top" class="r7-i" style="padding-left: 15px; padding-right: 15px;">
|
||||
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation">
|
||||
<tr>
|
||||
<td class="r17-c" align="center">
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="570" align="center" class="r3-o" style="table-layout: fixed; width: 570px;">
|
||||
<tr>
|
||||
<td valign="top">
|
||||
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation">
|
||||
<tr>
|
||||
<td class="r18-c" style="display: inline-block;">
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="570" class="r6-o" style="table-layout: fixed; width: 570px;">
|
||||
<tr>
|
||||
<td class="r19-i" style="padding-bottom: 15px; padding-left: 209px; padding-right: 209px; padding-top: 15px;">
|
||||
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation">
|
||||
<tr>
|
||||
<th width="40" class="r20-c mobshow resp-table" style="font-weight: normal;">
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r21-o" style="table-layout: fixed; width: 100%;">
|
||||
<tr>
|
||||
<td class="r22-i" style="font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px;"> <a href="https://github.com/makeplane" target="_blank" style="color: #006399; text-decoration: underline;"> <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/github_32px.png" width="32" border="0" style="display: block; width: 100%;"></a> </td>
|
||||
<td class="nl2go-responsive-hide" width="8" style="font-size: 0px; line-height: 1px;"> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</th>
|
||||
<th width="40" class="r20-c mobshow resp-table" style="font-weight: normal;">
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r21-o" style="table-layout: fixed; width: 100%;">
|
||||
<tr>
|
||||
<td class="r22-i" style="font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px;"> <a href="https://www.linkedin.com/company/planepowers/" target="_blank" style="color: #006399; text-decoration: underline;"> <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/linkedin_32px.png" width="32" border="0" style="display: block; width: 100%;"></a> </td>
|
||||
<td class="nl2go-responsive-hide" width="8" style="font-size: 0px; line-height: 1px;"> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</th>
|
||||
<th width="40" class="r20-c mobshow resp-table" style="font-weight: normal;">
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r21-o" style="table-layout: fixed; width: 100%;">
|
||||
<tr>
|
||||
<td class="r22-i" style="font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px;"> <a href="https://twitter.com/planepowers" target="_blank" style="color: #006399; text-decoration: underline;"> <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/twitter_32px.png" width="32" border="0" style="display: block; width: 100%;"></a> </td>
|
||||
<td class="nl2go-responsive-hide" width="8" style="font-size: 0px; line-height: 1px;"> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</th>
|
||||
<th width="32" class="r20-c mobshow resp-table" style="font-weight: normal;">
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r23-o" style="table-layout: fixed; width: 100%;">
|
||||
<tr>
|
||||
<td class="r22-i" style="font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px;"> <a href="https://plane.so/" target="_blank" style="color: #006399; text-decoration: underline;"> <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/website_32px.png" width="32" border="0" style="display: block; width: 100%;"></a> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</th>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</th>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" align="center" class="r3-o" style="table-layout: fixed; width: 100%;">
|
||||
<tr>
|
||||
<td class="r4-i" style="padding-bottom: 20px; padding-top: 20px;">
|
||||
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation">
|
||||
<tr>
|
||||
<th width="100%" valign="top" class="r5-c" style="font-weight: normal;">
|
||||
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r6-o" style="table-layout: fixed; width: 100%;">
|
||||
<tr>
|
||||
<td valign="top" class="r7-i" style="padding-left: 15px; padding-right: 15px;">
|
||||
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation">
|
||||
<tr>
|
||||
<td class="r12-c nl2go-default-textstyle" align="left" style="color: #3b3f44; font-family: georgia,serif; font-size: 18px; line-height: 1.5; word-break: break-word; padding-bottom: 15px; padding-top: 15px; text-align: left; valign: top;">
|
||||
<div>
|
||||
<p style="margin: 0; text-align: center;"><span style="font-family: Georgia, serif; font-size: 12px;">This email was sent to {{email}}. Please delete if you are not the intended recipient.</span></p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</th>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -44,7 +44,7 @@ const fetchDocument = async ({ context, documentName: pageId, instance }: FetchP
|
||||
await service.updateDescriptionBinary(pageId, payload);
|
||||
} catch (e) {
|
||||
const error = new AppError(e);
|
||||
logger.error("Failed to save binary after first conversion from html:", error);
|
||||
logger.error("Failed to save binary after first convertion from html:", error);
|
||||
}
|
||||
return convertedBinaryData;
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ export const AuthEmailForm = observer(function AuthEmailForm(props: TAuthEmailFo
|
||||
<div
|
||||
className={cn(
|
||||
`relative flex items-center rounded-md bg-surface-1 border`,
|
||||
!isFocused && Boolean(emailError?.email) ? `border-danger-strong` : `border-subtle`
|
||||
!isFocused && Boolean(emailError?.email) ? `border-red-500` : `border-subtle`
|
||||
)}
|
||||
onFocus={() => {
|
||||
setIsFocused(true);
|
||||
@@ -68,7 +68,7 @@ export const AuthEmailForm = observer(function AuthEmailForm(props: TAuthEmailFo
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="name@company.com"
|
||||
className={`disable-autofill-style h-10 w-full placeholder:text-placeholder autofill:bg-danger-subtle border-0 focus:bg-none active:bg-transparent`}
|
||||
className={`disable-autofill-style h-10 w-full placeholder:text-placeholder autofill:bg-red-500 border-0 focus:bg-none active:bg-transparent`}
|
||||
autoComplete="on"
|
||||
autoFocus
|
||||
ref={inputRef}
|
||||
@@ -88,7 +88,7 @@ export const AuthEmailForm = observer(function AuthEmailForm(props: TAuthEmailFo
|
||||
)}
|
||||
</div>
|
||||
{emailError?.email && !isFocused && (
|
||||
<p className="flex items-center gap-1 text-11 text-danger-primary px-0.5">
|
||||
<p className="flex items-center gap-1 text-11 text-red-600 px-0.5">
|
||||
<CircleAlert height={12} width={12} />
|
||||
{emailError.email}
|
||||
</p>
|
||||
|
||||
@@ -201,7 +201,7 @@ export const AuthPasswordForm = observer(function AuthPasswordForm(props: Props)
|
||||
</div>
|
||||
{!!passwordFormData.confirm_password &&
|
||||
passwordFormData.password !== passwordFormData.confirm_password &&
|
||||
renderPasswordMatchError && <span className="text-13 text-danger-primary">Passwords don{"'"}t match</span>}
|
||||
renderPasswordMatchError && <span className="text-13 text-red-500">Passwords don{"'"}t match</span>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ export function AuthUniqueCodeForm(props: TAuthUniqueCodeForm) {
|
||||
autoFocus
|
||||
/>
|
||||
<div className="flex w-full items-center justify-between px-1 text-11 pt-1">
|
||||
<p className="flex items-center gap-1 font-medium text-success-primary">
|
||||
<p className="flex items-center gap-1 font-medium text-green-700">
|
||||
<CircleCheck height={12} width={12} />
|
||||
Paste the code sent to your email
|
||||
</p>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
// plane imports
|
||||
import { CheckIcon } from "@plane/propel/icons";
|
||||
// lucide icons
|
||||
import { Check } from "lucide-react";
|
||||
|
||||
type Props = {
|
||||
icon?: React.ReactNode;
|
||||
@@ -24,7 +24,7 @@ export function FilterOption(props: Props) {
|
||||
isChecked ? "border-accent-strong bg-accent-primary text-on-color" : "border-strong"
|
||||
} ${multiple ? "rounded-xs" : "rounded-full"}`}
|
||||
>
|
||||
{isChecked && <CheckIcon width={10} height={10} strokeWidth={3} />}
|
||||
{isChecked && <Check size={10} strokeWidth={3} />}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 truncate">
|
||||
{icon && <div className="grid w-5 shrink-0 place-items-center">{icon}</div>}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { SearchIcon, CloseIcon } from "@plane/propel/icons";
|
||||
import { Search } from "lucide-react";
|
||||
import { CloseIcon } from "@plane/propel/icons";
|
||||
// types
|
||||
import type { IIssueFilterOptions, TIssueFilterKeys } from "@/types/issue";
|
||||
// local imports
|
||||
@@ -24,7 +25,7 @@ export const FilterSelection = observer(function FilterSelection(props: Props) {
|
||||
<div className="flex h-full w-full flex-col overflow-hidden">
|
||||
<div className="p-2.5 pb-0">
|
||||
<div className="flex items-center gap-1.5 rounded-sm border-[0.5px] border-subtle bg-surface-2 px-1.5 py-1 text-11">
|
||||
<SearchIcon className="text-placeholder" width={12} height={12} strokeWidth={2} />
|
||||
<Search className="text-placeholder" size={12} strokeWidth={2} />
|
||||
<input
|
||||
type="text"
|
||||
className="w-full bg-surface-2 outline-none placeholder:text-placeholder"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { observer } from "mobx-react";
|
||||
import { Paperclip } from "lucide-react";
|
||||
import { LinkIcon, ViewsIcon } from "@plane/propel/icons";
|
||||
import { Link, Paperclip } from "lucide-react";
|
||||
import { ViewsIcon } from "@plane/propel/icons";
|
||||
// plane imports
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import type { IIssueDisplayProperties } from "@plane/types";
|
||||
@@ -169,7 +169,7 @@ export const IssueProperties = observer(function IssueProperties(props: IIssuePr
|
||||
>
|
||||
<Tooltip tooltipHeading="Links" tooltipContent={`${issue.link_count}`}>
|
||||
<div className="flex h-5 flex-shrink-0 items-center justify-center gap-2 overflow-hidden rounded-sm border-[0.5px] border-strong px-2.5 py-1">
|
||||
<LinkIcon className="h-3 w-3 flex-shrink-0" strokeWidth={2} />
|
||||
<Link className="h-3 w-3 flex-shrink-0" strokeWidth={2} />
|
||||
<div className="text-11">{issue.link_count}</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
@@ -27,7 +27,7 @@ export const IssueBlockDate = observer(function IssueBlockDate(props: Props) {
|
||||
<Tooltip tooltipHeading="Due Date" tooltipContent={formattedDate}>
|
||||
<div
|
||||
className={cn("flex h-full items-center gap-1 rounded-sm px-2.5 py-1 text-11 text-primary", {
|
||||
"text-danger-primary": shouldHighLight && due_date && shouldHighlightIssueDueDate(due_date, state?.group),
|
||||
"text-red-500": shouldHighLight && due_date && shouldHighlightIssueDueDate(due_date, state?.group),
|
||||
"border-[0.5px] border-strong": shouldShowBorder,
|
||||
})}
|
||||
>
|
||||
|
||||
@@ -100,7 +100,7 @@ export const UserAvatar = observer(function UserAvatar() {
|
||||
type="submit"
|
||||
className="flex items-center gap-2 rounded-sm p-2 whitespace-nowrap hover:bg-layer-transparent-hover text-13 min-w-36 cursor-pointer"
|
||||
>
|
||||
<LogOut size={12} className="shrink-0 text-danger-primary" />
|
||||
<LogOut size={12} className="shrink-0 text-red-500" />
|
||||
<div>Sign out</div>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { MessageSquare, MoreVertical } from "lucide-react";
|
||||
import { Check, MessageSquare, MoreVertical } from "lucide-react";
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
// plane imports
|
||||
import type { EditorRefApi } from "@plane/editor";
|
||||
import { CheckIcon, CloseIcon } from "@plane/propel/icons";
|
||||
import { CloseIcon } from "@plane/propel/icons";
|
||||
import type { TIssuePublicComment } from "@plane/types";
|
||||
import { getFileURL } from "@plane/utils";
|
||||
// components
|
||||
@@ -132,16 +132,16 @@ export const CommentCard = observer(function CommentCard(props: Props) {
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="group rounded-sm border border-success-strong bg-success-primary p-2 shadow-md duration-300 hover:bg-success-primary"
|
||||
className="group rounded-sm border border-green-500 bg-green-500/20 p-2 shadow-md duration-300 hover:bg-green-500"
|
||||
>
|
||||
<CheckIcon className="h-3 w-3 text-on-color" strokeWidth={2} />
|
||||
<Check className="h-3 w-3 text-green-500 duration-300 group-hover:text-on-color" strokeWidth={2} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="group rounded-sm border border-danger-strong bg-danger-primary p-2 shadow-md duration-300 hover:bg-danger-primary-hover"
|
||||
className="group rounded-sm border border-red-500 bg-red-500/20 p-2 shadow-md duration-300 hover:bg-red-500"
|
||||
onClick={() => setIsEditing(false)}
|
||||
>
|
||||
<CloseIcon className="h-3 w-3 text-on-color" strokeWidth={2} />
|
||||
<CloseIcon className="h-3 w-3 text-red-500 duration-300 group-hover:text-on-color" strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { MoveRight } from "lucide-react";
|
||||
import { Link2, MoveRight } from "lucide-react";
|
||||
import { Listbox, Transition } from "@headlessui/react";
|
||||
// ui
|
||||
import { LinkIcon, CenterPanelIcon, FullScreenPanelIcon, SidePanelIcon } from "@plane/propel/icons";
|
||||
import { CenterPanelIcon, FullScreenPanelIcon, SidePanelIcon } from "@plane/propel/icons";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
// helpers
|
||||
import { copyTextToClipboard } from "@/helpers/string.helper";
|
||||
@@ -116,7 +116,7 @@ export const PeekOverviewHeader = observer(function PeekOverviewHeader(props: Pr
|
||||
className="shrink-0 focus:outline-none text-tertiary hover:text-secondary"
|
||||
tabIndex={1}
|
||||
>
|
||||
<LinkIcon className="h-4 w-4 -rotate-45" />
|
||||
<Link2 className="h-4 w-4 -rotate-45" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -114,7 +114,7 @@ export const PeekOverviewIssueProperties = observer(function PeekOverviewIssuePr
|
||||
{issueDetails.target_date ? (
|
||||
<div
|
||||
className={cn("flex items-center gap-1.5 rounded-sm py-0.5 text-11 text-primary", {
|
||||
"text-danger-primary": shouldHighlightIssueDueDate(issueDetails.target_date, state?.group),
|
||||
"text-red-500": shouldHighlightIssueDueDate(issueDetails.target_date, state?.group),
|
||||
})}
|
||||
>
|
||||
<DueDatePropertyIcon className="size-3" />
|
||||
|
||||
@@ -85,7 +85,7 @@ export const IssuePeekOverview = observer(function IssuePeekOverview(props: TIss
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 z-20 bg-backdrop transition-opacity" />
|
||||
<div className="fixed inset-0 z-20 bg-backdrop bg-opacity-50 transition-opacity" />
|
||||
</Transition.Child>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
|
||||
@@ -144,7 +144,7 @@ export const IssueVotes = observer(function IssueVotes(props: TIssueVotes) {
|
||||
"flex items-center justify-center gap-x-1 overflow-hidden rounded-sm border focus:outline-none hover:bg-layer-transparent-hover",
|
||||
votingDimensions,
|
||||
{
|
||||
"border-danger-strong text-danger-primary": isDownVotedByUser,
|
||||
"border-red-600 text-red-600": isDownVotedByUser,
|
||||
"border-strong": !isDownVotedByUser,
|
||||
"cursor-default": isInIframe,
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
"react-hook-form": "7.51.5",
|
||||
"react-popper": "^2.3.0",
|
||||
"react-router": "catalog:",
|
||||
"react-router-dom": "catalog:",
|
||||
"swr": "catalog:",
|
||||
"uuid": "catalog:"
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState, useEffect } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
// plane package imports
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { EUserPermissions, EUserPermissionsLevel, PROJECT_TRACKER_ELEMENTS } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { EmptyStateDetailed } from "@plane/propel/empty-state";
|
||||
import { Tabs } from "@plane/propel/tabs";
|
||||
@@ -11,6 +11,7 @@ import { cn } from "@plane/utils";
|
||||
import AnalyticsFilterActions from "@/components/analytics/analytics-filter-actions";
|
||||
import { PageHead } from "@/components/core/page-title";
|
||||
// hooks
|
||||
import { captureClick } from "@/helpers/event-tracker.helper";
|
||||
import { useCommandPalette } from "@/hooks/store/use-command-palette";
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
import { useWorkspace } from "@/hooks/store/use-workspace";
|
||||
@@ -119,6 +120,7 @@ function AnalyticsPage({ params }: Route.ComponentProps) {
|
||||
label: "Create a project",
|
||||
onClick: () => {
|
||||
toggleCreateProjectModal(true);
|
||||
captureClick({ elementName: PROJECT_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_PROJECT_BUTTON });
|
||||
},
|
||||
disabled: !canPerformEmptyStateActions,
|
||||
},
|
||||
|
||||
@@ -30,7 +30,7 @@ export const ProjectWorkItemDetailsHeader = observer(function ProjectWorkItemDet
|
||||
|
||||
return (
|
||||
<>
|
||||
{projectPreferences.navigationMode === "TABBED" && (
|
||||
{projectPreferences.navigationMode === "horizontal" && (
|
||||
<div className="z-20">
|
||||
<Row className="h-header flex gap-2 w-full items-center border-b border-subtle bg-surface-1">
|
||||
<div className="flex items-center gap-2 divide-x divide-subtle h-full w-full">
|
||||
|
||||
@@ -2,10 +2,10 @@ import { useCallback, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// plane imports
|
||||
import { Plus, Search } from "lucide-react";
|
||||
import { EUserPermissions, EUserPermissionsLevel, PROJECT_TRACKER_ELEMENTS } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { EmptyStateCompact } from "@plane/propel/empty-state";
|
||||
import { PlusIcon, SearchIcon } from "@plane/propel/icons";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { copyUrlToClipboard, orderJoinedProjects } from "@plane/utils";
|
||||
@@ -117,13 +117,13 @@ export const ExtendedProjectSidebar = observer(function ExtendedProjectSidebar()
|
||||
setIsProjectModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<PlusIcon className="size-3" />
|
||||
<Plus className="size-3" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<div className="ml-auto flex items-center gap-1.5 rounded-md border border-subtle bg-surface-1 px-2.5 py-1 w-full">
|
||||
<SearchIcon className="h-3.5 w-3.5 text-placeholder" />
|
||||
<Search className="h-3.5 w-3.5 text-placeholder" />
|
||||
<input
|
||||
className="w-full max-w-[234px] border-none bg-transparent text-13 outline-none placeholder:text-placeholder"
|
||||
placeholder={t("search")}
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ function ProjectLayout({ params }: Route.ComponentProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
{projectPreferences.navigationMode === "TABBED" && (
|
||||
{projectPreferences.navigationMode === "horizontal" && (
|
||||
<div className="z-20">
|
||||
<Row className="h-header flex gap-2 w-full items-center border-b border-subtle bg-surface-1">
|
||||
<div className="flex items-center gap-2 divide-x divide-subtle h-full w-full">
|
||||
|
||||
+22
-2
@@ -2,7 +2,7 @@ import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||
// constants
|
||||
import { EPageAccess } from "@plane/constants";
|
||||
import { EPageAccess, PROJECT_PAGE_TRACKER_EVENTS, PROJECT_TRACKER_ELEMENTS } from "@plane/constants";
|
||||
// plane types
|
||||
import { Button } from "@plane/propel/button";
|
||||
import { PageIcon } from "@plane/propel/icons";
|
||||
@@ -12,6 +12,7 @@ import type { TPage } from "@plane/types";
|
||||
import { Breadcrumbs, Header } from "@plane/ui";
|
||||
// helpers
|
||||
import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
|
||||
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
|
||||
// hooks
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
// plane web imports
|
||||
@@ -39,10 +40,23 @@ export const PagesListHeader = observer(function PagesListHeader() {
|
||||
|
||||
await createPage(payload)
|
||||
.then((res) => {
|
||||
captureSuccess({
|
||||
eventName: PROJECT_PAGE_TRACKER_EVENTS.create,
|
||||
payload: {
|
||||
id: res?.id,
|
||||
state: "SUCCESS",
|
||||
},
|
||||
});
|
||||
const pageId = `/${workspaceSlug}/projects/${currentProjectDetails?.id}/pages/${res?.id}`;
|
||||
router.push(pageId);
|
||||
})
|
||||
.catch((err) => {
|
||||
captureError({
|
||||
eventName: PROJECT_PAGE_TRACKER_EVENTS.create,
|
||||
payload: {
|
||||
state: "ERROR",
|
||||
},
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
@@ -72,7 +86,13 @@ export const PagesListHeader = observer(function PagesListHeader() {
|
||||
</Header.LeftItem>
|
||||
{canCurrentUserCreatePage && (
|
||||
<Header.RightItem>
|
||||
<Button variant="primary" size="lg" onClick={handleCreatePage} loading={isCreatingPage}>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
onClick={handleCreatePage}
|
||||
loading={isCreatingPage}
|
||||
data-ph-element={PROJECT_TRACKER_ELEMENTS.CREATE_HEADER_BUTTON}
|
||||
>
|
||||
{isCreatingPage ? "Adding" : "Add page"}
|
||||
</Button>
|
||||
</Header.RightItem>
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useRef } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
|
||||
import { Lock } from "lucide-react";
|
||||
// plane imports
|
||||
import {
|
||||
EIssueFilterType,
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
WORK_ITEM_TRACKER_ELEMENTS,
|
||||
} from "@plane/constants";
|
||||
import { Button } from "@plane/propel/button";
|
||||
import { LockIcon, ViewsIcon } from "@plane/propel/icons";
|
||||
import { ViewsIcon } from "@plane/propel/icons";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import type { ICustomSearchSelectOption, IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types";
|
||||
import { EIssuesStoreType, EViewAccess, EIssueLayoutTypes } from "@plane/types";
|
||||
@@ -151,7 +151,7 @@ export const ProjectViewIssuesHeader = observer(function ProjectViewIssuesHeader
|
||||
{viewDetails?.access === EViewAccess.PRIVATE ? (
|
||||
<div className="cursor-default text-tertiary">
|
||||
<Tooltip tooltipContent={"Private"}>
|
||||
<LockIcon className="h-4 w-4" />
|
||||
<Lock className="h-4 w-4" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { useTheme } from "next-themes";
|
||||
// plane imports
|
||||
import { HEADER_GITHUB_ICON, GITHUB_REDIRECTED_TRACKER_EVENT } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// assets
|
||||
import githubBlackImage from "@/app/assets/logos/github-black.png?url";
|
||||
import githubWhiteImage from "@/app/assets/logos/github-white.png?url";
|
||||
// helpers
|
||||
import { captureElementAndEvent } from "@/helpers/event-tracker.helper";
|
||||
|
||||
export function StarUsOnGitHubLink() {
|
||||
// plane hooks
|
||||
@@ -15,6 +18,17 @@ export function StarUsOnGitHubLink() {
|
||||
return (
|
||||
<a
|
||||
aria-label={t("home.star_us_on_github")}
|
||||
onClick={() =>
|
||||
captureElementAndEvent({
|
||||
element: {
|
||||
elementName: HEADER_GITHUB_ICON,
|
||||
},
|
||||
event: {
|
||||
eventName: GITHUB_REDIRECTED_TRACKER_EVENT,
|
||||
state: "SUCCESS",
|
||||
},
|
||||
})
|
||||
}
|
||||
className="flex flex-shrink-0 items-center gap-1.5 rounded-sm bg-layer-2 px-3 py-1.5"
|
||||
href="https://github.com/makeplane/plane"
|
||||
target="_blank"
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Search } from "lucide-react";
|
||||
// plane imports
|
||||
import { DEFAULT_GLOBAL_VIEWS_LIST } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { SearchIcon } from "@plane/propel/icons";
|
||||
import { Input } from "@plane/ui";
|
||||
// components
|
||||
import { PageHead } from "@/components/core/page-title";
|
||||
@@ -25,7 +25,7 @@ function WorkspaceViewsPage() {
|
||||
<PageHead title={pageTitle} />
|
||||
<div className="flex flex-col h-full w-full overflow-hidden">
|
||||
<div className="flex h-11 w-full items-center gap-2.5 px-5 py-3 overflow-hidden border-b border-subtle">
|
||||
<SearchIcon className="text-secondary" width={14} height={14} strokeWidth={2} />
|
||||
<Search className="text-secondary" size={14} strokeWidth={2} />
|
||||
<Input
|
||||
className="w-full bg-transparent !p-0 text-11 leading-5 text-secondary placeholder:text-placeholder focus:outline-none"
|
||||
value={query}
|
||||
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import useSWR from "swr";
|
||||
// plane imports
|
||||
import { EUserPermissions, EUserPermissionsLevel, WORKSPACE_SETTINGS_TRACKER_ELEMENTS } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { EmptyStateCompact } from "@plane/propel/empty-state";
|
||||
import { WorkspaceAPITokenService } from "@plane/services";
|
||||
// component
|
||||
import { CreateApiTokenModal } from "@/components/api-token/modal/create-token-modal";
|
||||
import { ApiTokenListItem } from "@/components/api-token/token-list-item";
|
||||
import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view";
|
||||
import { PageHead } from "@/components/core/page-title";
|
||||
import { SettingsContentWrapper } from "@/components/settings/content-wrapper";
|
||||
import { SettingsHeading } from "@/components/settings/heading";
|
||||
import { APITokenSettingsLoader } from "@/components/ui/loader/settings/api-token";
|
||||
// constants
|
||||
import { WORKSPACE_API_TOKENS_LIST } from "@/constants/fetch-keys";
|
||||
// helpers
|
||||
import { captureClick } from "@/helpers/event-tracker.helper";
|
||||
// store hooks
|
||||
import { useWorkspace } from "@/hooks/store/use-workspace";
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
import type { Route } from "./+types/page";
|
||||
|
||||
const workspaceApiTokenService = new WorkspaceAPITokenService();
|
||||
|
||||
function ApiTokensPage({ params }: Route.ComponentProps) {
|
||||
// states
|
||||
const [isCreateTokenModalOpen, setIsCreateTokenModalOpen] = useState(false);
|
||||
// router
|
||||
const { workspaceSlug } = params;
|
||||
// plane hooks
|
||||
const { t } = useTranslation();
|
||||
// store hooks
|
||||
const { workspaceUserInfo, allowPermissions } = useUserPermissions();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
// derived values
|
||||
const canPerformWorkspaceAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
|
||||
|
||||
const { data: tokens } = useSWR(
|
||||
canPerformWorkspaceAdminActions ? WORKSPACE_API_TOKENS_LIST(workspaceSlug) : null,
|
||||
canPerformWorkspaceAdminActions ? () => workspaceApiTokenService.list(workspaceSlug) : null
|
||||
);
|
||||
|
||||
const pageTitle = currentWorkspace?.name
|
||||
? `${currentWorkspace.name} - ${t("workspace_settings.settings.api_tokens.title")}`
|
||||
: undefined;
|
||||
|
||||
if (workspaceUserInfo && !canPerformWorkspaceAdminActions) {
|
||||
return <NotAuthorizedView section="settings" className="h-auto" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsContentWrapper>
|
||||
<PageHead title={pageTitle} />
|
||||
{!tokens ? (
|
||||
<APITokenSettingsLoader title={t("workspace_settings.settings.api_tokens.title")} />
|
||||
) : (
|
||||
<div className="w-full">
|
||||
<CreateApiTokenModal
|
||||
isOpen={isCreateTokenModalOpen}
|
||||
onClose={() => setIsCreateTokenModalOpen(false)}
|
||||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
<SettingsHeading
|
||||
title={t("workspace_settings.settings.api_tokens.heading")}
|
||||
description={t("workspace_settings.settings.api_tokens.description")}
|
||||
button={{
|
||||
label: t("workspace_settings.settings.api_tokens.add_token"),
|
||||
onClick: () => {
|
||||
captureClick({
|
||||
elementName: WORKSPACE_SETTINGS_TRACKER_ELEMENTS.HEADER_ADD_PAT_BUTTON,
|
||||
});
|
||||
setIsCreateTokenModalOpen(true);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{tokens.length > 0 ? (
|
||||
<div className="flex h-full w-full flex-col">
|
||||
<div className="h-full w-full overflow-y-auto">
|
||||
{tokens.map((token) => (
|
||||
<ApiTokenListItem key={token.id} token={token} workspaceSlug={workspaceSlug} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-full w-full flex-col">
|
||||
<div className="h-full w-full flex items-center justify-center">
|
||||
<EmptyStateCompact
|
||||
assetKey="token"
|
||||
title={t("settings_empty_state.tokens.title")}
|
||||
description={t("settings_empty_state.tokens.description")}
|
||||
actions={[
|
||||
{
|
||||
label: t("settings_empty_state.tokens.cta_primary"),
|
||||
onClick: () => {
|
||||
captureClick({
|
||||
elementName: WORKSPACE_SETTINGS_TRACKER_ELEMENTS.EMPTY_STATE_ADD_PAT_BUTTON,
|
||||
});
|
||||
setIsCreateTokenModalOpen(true);
|
||||
},
|
||||
},
|
||||
]}
|
||||
align="start"
|
||||
rootClassName="py-20"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</SettingsContentWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(ApiTokensPage);
|
||||
@@ -8,6 +8,8 @@ import { SettingsMobileNav } from "@/components/settings/mobile";
|
||||
// plane imports
|
||||
import { WORKSPACE_SETTINGS_ACCESS } from "@plane/constants";
|
||||
import type { EUserWorkspaceRoles } from "@plane/types";
|
||||
// plane web components
|
||||
import { WorkspaceSettingsRightSidebar } from "@/plane-web/components/workspace/right-sidebar";
|
||||
// hooks
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
// local components
|
||||
@@ -46,6 +48,7 @@ const WorkspaceSettingLayout = observer(function WorkspaceSettingLayout({ params
|
||||
<div className="w-full h-full overflow-y-scroll md:pt-page-y">
|
||||
<Outlet />
|
||||
</div>
|
||||
<WorkspaceSettingsRightSidebar workspaceSlug={workspaceSlug} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+33
-5
@@ -1,10 +1,15 @@
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Search } from "lucide-react";
|
||||
// types
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import {
|
||||
EUserPermissions,
|
||||
EUserPermissionsLevel,
|
||||
MEMBER_TRACKER_ELEMENTS,
|
||||
MEMBER_TRACKER_EVENTS,
|
||||
} from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Button } from "@plane/propel/button";
|
||||
import { SearchIcon } from "@plane/propel/icons";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import type { IWorkspaceBulkInviteFormData } from "@plane/types";
|
||||
import { cn } from "@plane/utils";
|
||||
@@ -15,6 +20,8 @@ import { PageHead } from "@/components/core/page-title";
|
||||
import { MemberListFiltersDropdown } from "@/components/project/dropdowns/filters/member-list";
|
||||
import { SettingsContentWrapper } from "@/components/settings/content-wrapper";
|
||||
import { WorkspaceMembersList } from "@/components/workspace/settings/members-list";
|
||||
// helpers
|
||||
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
|
||||
// hooks
|
||||
import { useMember } from "@/hooks/store/use-member";
|
||||
import { useWorkspace } from "@/hooks/store/use-workspace";
|
||||
@@ -35,7 +42,7 @@ const WorkspaceMembersSettingsPage = observer(function WorkspaceMembersSettingsP
|
||||
const {
|
||||
workspace: { workspaceMemberIds, inviteMembersToWorkspace, filtersStore },
|
||||
} = useMember();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { currentWorkspace, mutateWorkspaceMembersActivity } = useWorkspace();
|
||||
const { t } = useTranslation();
|
||||
|
||||
// derived values
|
||||
@@ -48,9 +55,17 @@ const WorkspaceMembersSettingsPage = observer(function WorkspaceMembersSettingsP
|
||||
const handleWorkspaceInvite = async (data: IWorkspaceBulkInviteFormData) => {
|
||||
try {
|
||||
await inviteMembersToWorkspace(workspaceSlug, data);
|
||||
void mutateWorkspaceMembersActivity(workspaceSlug);
|
||||
|
||||
setInviteModal(false);
|
||||
|
||||
captureSuccess({
|
||||
eventName: MEMBER_TRACKER_EVENTS.invite,
|
||||
payload: {
|
||||
emails: data.emails.map((email) => email.email),
|
||||
},
|
||||
});
|
||||
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
@@ -62,6 +77,14 @@ const WorkspaceMembersSettingsPage = observer(function WorkspaceMembersSettingsP
|
||||
const err = error as Error & { error?: string };
|
||||
message = err.error;
|
||||
}
|
||||
captureError({
|
||||
eventName: MEMBER_TRACKER_EVENTS.invite,
|
||||
payload: {
|
||||
emails: data.emails.map((email) => email.email),
|
||||
},
|
||||
error: error as Error,
|
||||
});
|
||||
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
@@ -114,7 +137,7 @@ const WorkspaceMembersSettingsPage = observer(function WorkspaceMembersSettingsP
|
||||
</h4>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1.5 rounded-md border border-subtle bg-surface-1 px-2.5 py-1.5">
|
||||
<SearchIcon className="h-3.5 w-3.5 text-placeholder" />
|
||||
<Search className="h-3.5 w-3.5 text-placeholder" />
|
||||
<input
|
||||
className="w-full max-w-[234px] border-none bg-transparent text-body-xs-regular outline-none placeholder:text-placeholder"
|
||||
placeholder={`${t("search")}...`}
|
||||
@@ -131,7 +154,12 @@ const WorkspaceMembersSettingsPage = observer(function WorkspaceMembersSettingsP
|
||||
/>
|
||||
<MembersActivityButton workspaceSlug={workspaceSlug} />
|
||||
{canPerformWorkspaceAdminActions && (
|
||||
<Button variant="primary" size="lg" onClick={() => setInviteModal(true)}>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
onClick={() => setInviteModal(true)}
|
||||
data-ph-element={MEMBER_TRACKER_ELEMENTS.HEADER_ADD_BUTTON}
|
||||
>
|
||||
{t("workspace_settings.settings.members.add_member")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
import { ArrowUpToLine, Building, CreditCard, Users, Webhook } from "lucide-react";
|
||||
import { ArrowUpToLine, Building, CreditCard, KeyRound, Users, Webhook } from "lucide-react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
// plane imports
|
||||
import {
|
||||
@@ -25,6 +25,7 @@ export const WORKSPACE_SETTINGS_ICONS: Record<keyof typeof WORKSPACE_SETTINGS, L
|
||||
export: ArrowUpToLine,
|
||||
"billing-and-plans": CreditCard,
|
||||
webhooks: Webhook,
|
||||
"api-tokens": KeyRound,
|
||||
};
|
||||
|
||||
export function WorkspaceActionIcons({ type, size, className }: { type: string; size?: number; className?: string }) {
|
||||
|
||||
+14
-1
@@ -1,7 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import useSWR from "swr";
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { EUserPermissions, EUserPermissionsLevel, WORKSPACE_SETTINGS_TRACKER_EVENTS } from "@plane/constants";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import type { IWebhook } from "@plane/types";
|
||||
// ui
|
||||
@@ -11,6 +11,7 @@ import { PageHead } from "@/components/core/page-title";
|
||||
import { SettingsContentWrapper } from "@/components/settings/content-wrapper";
|
||||
import { DeleteWebhookModal, WebhookDeleteSection, WebhookForm } from "@/components/web-hooks";
|
||||
// hooks
|
||||
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
|
||||
import { useWebhook } from "@/hooks/store/use-webhook";
|
||||
import { useWorkspace } from "@/hooks/store/use-workspace";
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
@@ -54,6 +55,12 @@ function WebhookDetailsPage({ params }: Route.ComponentProps) {
|
||||
|
||||
try {
|
||||
await updateWebhook(workspaceSlug, formData.id, payload);
|
||||
|
||||
captureSuccess({
|
||||
eventName: WORKSPACE_SETTINGS_TRACKER_EVENTS.webhook_updated,
|
||||
payload: { webhook: formData.id },
|
||||
});
|
||||
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
@@ -61,6 +68,12 @@ function WebhookDetailsPage({ params }: Route.ComponentProps) {
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
captureError({
|
||||
eventName: WORKSPACE_SETTINGS_TRACKER_EVENTS.webhook_updated,
|
||||
payload: { webhook: formData.id },
|
||||
error: error as Error,
|
||||
});
|
||||
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
|
||||
+8
-1
@@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import useSWR from "swr";
|
||||
// plane imports
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { EUserPermissions, EUserPermissionsLevel, WORKSPACE_SETTINGS_TRACKER_ELEMENTS } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// components
|
||||
import { EmptyStateCompact } from "@plane/propel/empty-state";
|
||||
@@ -13,6 +13,7 @@ import { SettingsHeading } from "@/components/settings/heading";
|
||||
import { WebhookSettingsLoader } from "@/components/ui/loader/settings/web-hook";
|
||||
import { WebhooksList, CreateWebhookModal } from "@/components/web-hooks";
|
||||
// hooks
|
||||
import { captureClick } from "@/helpers/event-tracker.helper";
|
||||
import { useWebhook } from "@/hooks/store/use-webhook";
|
||||
import { useWorkspace } from "@/hooks/store/use-workspace";
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
@@ -71,6 +72,9 @@ function WebhooksListPage({ params }: Route.ComponentProps) {
|
||||
button={{
|
||||
label: t("workspace_settings.settings.webhooks.add_webhook"),
|
||||
onClick: () => {
|
||||
captureClick({
|
||||
elementName: WORKSPACE_SETTINGS_TRACKER_ELEMENTS.HEADER_ADD_WEBHOOK_BUTTON,
|
||||
});
|
||||
setShowCreateWebhookModal(true);
|
||||
},
|
||||
}}
|
||||
@@ -90,6 +94,9 @@ function WebhooksListPage({ params }: Route.ComponentProps) {
|
||||
{
|
||||
label: t("settings_empty_state.webhooks.cta_primary"),
|
||||
onClick: () => {
|
||||
captureClick({
|
||||
elementName: WORKSPACE_SETTINGS_TRACKER_ELEMENTS.EMPTY_STATE_ADD_WEBHOOK_BUTTON,
|
||||
});
|
||||
setShowCreateWebhookModal(true);
|
||||
},
|
||||
},
|
||||
|
||||
+40
-47
@@ -1,7 +1,9 @@
|
||||
import { useState } from "react";
|
||||
import type { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import useSWR from "swr";
|
||||
// plane imports
|
||||
import { PROFILE_SETTINGS_TRACKER_ELEMENTS } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// component
|
||||
import { EmptyStateCompact } from "@plane/propel/empty-state";
|
||||
@@ -13,14 +15,14 @@ import { SettingsHeading } from "@/components/settings/heading";
|
||||
import { APITokenSettingsLoader } from "@/components/ui/loader/settings/api-token";
|
||||
import { API_TOKENS_LIST } from "@/constants/fetch-keys";
|
||||
// store hooks
|
||||
import { captureClick } from "@/helpers/event-tracker.helper";
|
||||
import { useWorkspace } from "@/hooks/store/use-workspace";
|
||||
|
||||
const apiTokenService = new APITokenService();
|
||||
|
||||
function ApiTokensPage() {
|
||||
const ApiTokensPage: FC = observer(function ApiTokensPage() {
|
||||
// states
|
||||
const [isCreateTokenModalOpen, setIsCreateTokenModalOpen] = useState(false);
|
||||
// router
|
||||
// plane hooks
|
||||
const { t } = useTranslation();
|
||||
// store hooks
|
||||
@@ -29,11 +31,11 @@ function ApiTokensPage() {
|
||||
const { data: tokens } = useSWR(API_TOKENS_LIST, () => apiTokenService.list());
|
||||
|
||||
const pageTitle = currentWorkspace?.name
|
||||
? `${currentWorkspace.name} - ${t("workspace_settings.settings.api_tokens.title")}`
|
||||
? `${currentWorkspace.name} - ${t("account_settings.api_tokens.title")}`
|
||||
: undefined;
|
||||
|
||||
if (!tokens) {
|
||||
return <APITokenSettingsLoader />;
|
||||
return <APITokenSettingsLoader title={t("account_settings.api_tokens.title")} />;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -41,58 +43,49 @@ function ApiTokensPage() {
|
||||
<PageHead title={pageTitle} />
|
||||
<CreateApiTokenModal isOpen={isCreateTokenModalOpen} onClose={() => setIsCreateTokenModalOpen(false)} />
|
||||
<section className="w-full">
|
||||
<SettingsHeading
|
||||
title={t("account_settings.api_tokens.heading")}
|
||||
description={t("account_settings.api_tokens.description")}
|
||||
button={{
|
||||
label: t("workspace_settings.settings.api_tokens.add_token"),
|
||||
onClick: () => {
|
||||
captureClick({
|
||||
elementName: PROFILE_SETTINGS_TRACKER_ELEMENTS.HEADER_ADD_PAT_BUTTON,
|
||||
});
|
||||
setIsCreateTokenModalOpen(true);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{tokens.length > 0 ? (
|
||||
<>
|
||||
<SettingsHeading
|
||||
title={t("account_settings.api_tokens.heading")}
|
||||
description={t("account_settings.api_tokens.description")}
|
||||
button={{
|
||||
label: t("workspace_settings.settings.api_tokens.add_token"),
|
||||
onClick: () => {
|
||||
setIsCreateTokenModalOpen(true);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
{tokens.map((token) => (
|
||||
<ApiTokenListItem key={token.id} token={token} />
|
||||
))}
|
||||
</div>
|
||||
{tokens.map((token) => (
|
||||
<ApiTokenListItem key={token.id} token={token} />
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<div className="flex h-full w-full flex-col py-">
|
||||
<SettingsHeading
|
||||
title={t("account_settings.api_tokens.heading")}
|
||||
description={t("account_settings.api_tokens.description")}
|
||||
button={{
|
||||
label: t("workspace_settings.settings.api_tokens.add_token"),
|
||||
<EmptyStateCompact
|
||||
assetKey="token"
|
||||
assetClassName="size-20"
|
||||
title={t("settings_empty_state.tokens.title")}
|
||||
description={t("settings_empty_state.tokens.description")}
|
||||
actions={[
|
||||
{
|
||||
label: t("settings_empty_state.tokens.cta_primary"),
|
||||
onClick: () => {
|
||||
captureClick({
|
||||
elementName: PROFILE_SETTINGS_TRACKER_ELEMENTS.EMPTY_STATE_ADD_PAT_BUTTON,
|
||||
});
|
||||
setIsCreateTokenModalOpen(true);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<EmptyStateCompact
|
||||
assetKey="token"
|
||||
assetClassName="size-20"
|
||||
title={t("settings_empty_state.tokens.title")}
|
||||
description={t("settings_empty_state.tokens.description")}
|
||||
actions={[
|
||||
{
|
||||
label: t("settings_empty_state.tokens.cta_primary"),
|
||||
onClick: () => {
|
||||
setIsCreateTokenModalOpen(true);
|
||||
},
|
||||
},
|
||||
]}
|
||||
align="start"
|
||||
rootClassName="py-20"
|
||||
/>
|
||||
</div>
|
||||
},
|
||||
]}
|
||||
align="start"
|
||||
rootClassName="py-20"
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default observer(ApiTokensPage);
|
||||
export default ApiTokensPage;
|
||||
|
||||
@@ -159,9 +159,7 @@ function SecurityPage() {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{errors.old_password && (
|
||||
<span className="text-11 text-danger-primary">{errors.old_password.message}</span>
|
||||
)}
|
||||
{errors.old_password && <span className="text-11 text-red-500">{errors.old_password.message}</span>}
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-1">
|
||||
@@ -201,9 +199,7 @@ function SecurityPage() {
|
||||
</div>
|
||||
{passwordSupport}
|
||||
{isNewPasswordSameAsOldPassword && !isPasswordInputFocused && (
|
||||
<span className="text-11 text-danger-primary">
|
||||
{t("new_password_must_be_different_from_old_password")}
|
||||
</span>
|
||||
<span className="text-11 text-red-500">{t("new_password_must_be_different_from_old_password")}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
@@ -242,7 +238,7 @@ function SecurityPage() {
|
||||
)}
|
||||
</div>
|
||||
{!!confirmPassword && password !== confirmPassword && renderPasswordMatchError && (
|
||||
<span className="text-13 text-danger-primary">{t("auth.common.password.errors.match")}</span>
|
||||
<span className="text-13 text-red-500">{t("auth.common.password.errors.match")}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
import { CircleUser, Activity, Bell, CircleUserRound, KeyRound, Settings2, Blocks } from "lucide-react";
|
||||
import { CircleUser, Activity, Bell, CircleUserRound, KeyRound, Settings2, Blocks, Lock } from "lucide-react";
|
||||
// plane imports
|
||||
import { GROUPED_PROFILE_SETTINGS, PROFILE_SETTINGS_CATEGORIES } from "@plane/constants";
|
||||
import { LockIcon } from "@plane/propel/icons";
|
||||
import { getFileURL } from "@plane/utils";
|
||||
// components
|
||||
import { SettingsSidebar } from "@/components/settings/sidebar";
|
||||
@@ -12,7 +11,7 @@ import { useUser } from "@/hooks/store/user";
|
||||
|
||||
const ICONS = {
|
||||
profile: CircleUser,
|
||||
security: LockIcon,
|
||||
security: Lock,
|
||||
activity: Activity,
|
||||
preferences: Settings2,
|
||||
notifications: Bell,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useState } from "react";
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
|
||||
import useSWR, { mutate } from "swr";
|
||||
import { CheckCircle2 } from "lucide-react";
|
||||
// plane imports
|
||||
import { ROLE } from "@plane/constants";
|
||||
import { ROLE, MEMBER_TRACKER_EVENTS, MEMBER_TRACKER_ELEMENTS, GROUP_WORKSPACE_TRACKER_EVENT } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// types
|
||||
import { Button } from "@plane/propel/button";
|
||||
@@ -19,7 +19,9 @@ import emptyInvitation from "@/app/assets/empty-state/invitation.svg?url";
|
||||
import { EmptyState } from "@/components/common/empty-state";
|
||||
import { WorkspaceLogo } from "@/components/workspace/logo";
|
||||
import { USER_WORKSPACES_LIST } from "@/constants/fetch-keys";
|
||||
// helpers
|
||||
// hooks
|
||||
import { captureError, captureSuccess, joinEventGroup } from "@/helpers/event-tracker.helper";
|
||||
import { useWorkspace } from "@/hooks/store/use-workspace";
|
||||
import { useUser, useUserProfile } from "@/hooks/store/user";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
@@ -77,6 +79,18 @@ function UserInvitationsPage() {
|
||||
const firstInviteId = invitationsRespond[0];
|
||||
const invitation = invitations?.find((i) => i.id === firstInviteId);
|
||||
const redirectWorkspace = invitations?.find((i) => i.id === firstInviteId)?.workspace;
|
||||
if (redirectWorkspace?.id) {
|
||||
joinEventGroup(GROUP_WORKSPACE_TRACKER_EVENT, redirectWorkspace?.id, {
|
||||
date: new Date().toDateString(),
|
||||
workspace_id: redirectWorkspace?.id,
|
||||
});
|
||||
}
|
||||
captureSuccess({
|
||||
eventName: MEMBER_TRACKER_EVENTS.accept,
|
||||
payload: {
|
||||
member_id: invitation?.id,
|
||||
},
|
||||
});
|
||||
updateUserProfile({ last_workspace_id: redirectWorkspace?.id })
|
||||
.then(() => {
|
||||
setIsJoiningWorkspaces(false);
|
||||
@@ -93,7 +107,14 @@ function UserInvitationsPage() {
|
||||
setIsJoiningWorkspaces(false);
|
||||
});
|
||||
})
|
||||
.catch((_err) => {
|
||||
.catch((err) => {
|
||||
captureError({
|
||||
eventName: MEMBER_TRACKER_EVENTS.accept,
|
||||
payload: {
|
||||
member_id: invitationsRespond?.[0],
|
||||
},
|
||||
error: err,
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: t("error"),
|
||||
@@ -162,6 +183,7 @@ function UserInvitationsPage() {
|
||||
onClick={submitInvitations}
|
||||
disabled={isJoiningWorkspaces || invitationsRespond.length === 0}
|
||||
loading={isJoiningWorkspaces}
|
||||
data-ph-element={MEMBER_TRACKER_ELEMENTS.ACCEPT_INVITATION_BUTTON}
|
||||
>
|
||||
{t("accept_and_join")}
|
||||
</Button>
|
||||
|
||||
@@ -157,9 +157,7 @@ function SecurityPage() {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{errors.old_password && (
|
||||
<span className="text-11 text-danger-primary">{errors.old_password.message}</span>
|
||||
)}
|
||||
{errors.old_password && <span className="text-11 text-red-500">{errors.old_password.message}</span>}
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-1">
|
||||
@@ -199,9 +197,7 @@ function SecurityPage() {
|
||||
</div>
|
||||
{passwordSupport}
|
||||
{isNewPasswordSameAsOldPassword && !isPasswordInputFocused && (
|
||||
<span className="text-11 text-danger-primary">
|
||||
{t("new_password_must_be_different_from_old_password")}
|
||||
</span>
|
||||
<span className="text-11 text-red-500">{t("new_password_must_be_different_from_old_password")}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
@@ -240,7 +236,7 @@ function SecurityPage() {
|
||||
)}
|
||||
</div>
|
||||
{!!confirmPassword && password !== confirmPassword && renderPasswordMatchError && (
|
||||
<span className="text-13 text-danger-primary">{t("auth.common.password.errors.match")}</span>
|
||||
<span className="text-13 text-red-500">{t("auth.common.password.errors.match")}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -249,7 +249,7 @@ export const ProfileLayoutSidebar = observer(function ProfileLayoutSidebar() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSignOut}
|
||||
className="flex items-center justify-center gap-2 text-13 font-medium text-danger-primary"
|
||||
className="flex items-center justify-center gap-2 text-13 font-medium text-red-500"
|
||||
disabled={isSigningOut}
|
||||
>
|
||||
<LogOut className="h-3.5 w-3.5" />
|
||||
|
||||
@@ -2,8 +2,8 @@ import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
import { Boxes, Share2, Star, User2 } from "lucide-react";
|
||||
import { CheckIcon, CloseIcon } from "@plane/propel/icons";
|
||||
import { Boxes, Check, Share2, Star, User2 } from "lucide-react";
|
||||
import { CloseIcon } from "@plane/propel/icons";
|
||||
// components
|
||||
import { LogoSpinner } from "@/components/common/logo-spinner";
|
||||
import { EmptySpace, EmptySpaceItem } from "@/components/ui/empty-space";
|
||||
@@ -83,7 +83,7 @@ function WorkspaceInvitationPage() {
|
||||
title={`You have been invited to ${invitationDetail.workspace.name}`}
|
||||
description="Your workspace is where you'll create projects, collaborate on your work items, and organize different streams of work in your Plane account."
|
||||
>
|
||||
<EmptySpaceItem Icon={CheckIcon} title="Accept" action={handleAccept} />
|
||||
<EmptySpaceItem Icon={Check} title="Accept" action={handleAccept} />
|
||||
<EmptySpaceItem Icon={CloseIcon} title="Ignore" action={handleReject} />
|
||||
</EmptySpace>
|
||||
)
|
||||
|
||||
@@ -46,7 +46,7 @@ export function DevErrorComponent({ error, onGoHome, onReload }: DevErrorCompone
|
||||
<Card variant={ECardVariant.WITH_SHADOW} className="!p-6 transition-none">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h2 className="text-20 font-semibold text-danger-primary mb-2">
|
||||
<h2 className="text-20 font-semibold text-red-500 mb-2">
|
||||
{error.status} {error.statusText}
|
||||
</h2>
|
||||
<div className="h-px w-full bg-subtle-1" />
|
||||
@@ -80,7 +80,7 @@ export function DevErrorComponent({ error, onGoHome, onReload }: DevErrorCompone
|
||||
<Card variant={ECardVariant.WITH_SHADOW} className="!p-6 transition-none">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h2 className="text-20 font-semibold text-danger-primary mb-2">Error</h2>
|
||||
<h2 className="text-20 font-semibold text-red-500 mb-2">Error</h2>
|
||||
<div className="h-px w-full bg-subtle-1" />
|
||||
</div>
|
||||
|
||||
|
||||
+18
-13
@@ -1,5 +1,5 @@
|
||||
import { lazy, Suspense } from "react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useTheme, ThemeProvider } from "next-themes";
|
||||
import { SWRConfig } from "swr";
|
||||
// Plane Imports
|
||||
import { WEB_SWR_CONFIG } from "@plane/constants";
|
||||
@@ -9,20 +9,20 @@ import { Toast } from "@plane/propel/toast";
|
||||
import { resolveGeneralTheme } from "@plane/utils";
|
||||
// polyfills
|
||||
import "@/lib/polyfills";
|
||||
// progress bar
|
||||
import { AppProgressBar } from "@/lib/b-progress";
|
||||
// mobx store provider
|
||||
import { StoreProvider } from "@/lib/store-context";
|
||||
// wrappers
|
||||
import { InstanceWrapper } from "@/lib/wrappers/instance-wrapper";
|
||||
|
||||
// lazy imports
|
||||
const AppProgressBar = lazy(function AppProgressBar() {
|
||||
return import("@/lib/b-progress/AppProgressBar");
|
||||
});
|
||||
|
||||
const StoreWrapper = lazy(function StoreWrapper() {
|
||||
return import("@/lib/wrappers/store-wrapper");
|
||||
});
|
||||
|
||||
const InstanceWrapper = lazy(function InstanceWrapper() {
|
||||
return import("@/lib/wrappers/instance-wrapper");
|
||||
const PostHogProvider = lazy(function PostHogProvider() {
|
||||
return import("@/lib/posthog-provider");
|
||||
});
|
||||
|
||||
const ChatSupportModal = lazy(function ChatSupportModal() {
|
||||
@@ -33,27 +33,32 @@ export interface IAppProvider {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
function ToastWithTheme() {
|
||||
const { resolvedTheme } = useTheme();
|
||||
return <Toast theme={resolveGeneralTheme(resolvedTheme)} />;
|
||||
}
|
||||
|
||||
export function AppProvider(props: IAppProvider) {
|
||||
const { children } = props;
|
||||
// themes
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<StoreProvider>
|
||||
<>
|
||||
<ThemeProvider themes={["light", "dark", "light-contrast", "dark-contrast", "custom"]} defaultTheme="system">
|
||||
<AppProgressBar />
|
||||
<TranslationProvider>
|
||||
<Toast theme={resolveGeneralTheme(resolvedTheme)} />
|
||||
<ToastWithTheme />
|
||||
<StoreWrapper>
|
||||
<InstanceWrapper>
|
||||
<Suspense>
|
||||
<ChatSupportModal />
|
||||
<SWRConfig value={WEB_SWR_CONFIG}>{children}</SWRConfig>
|
||||
<PostHogProvider>
|
||||
<SWRConfig value={WEB_SWR_CONFIG}>{children}</SWRConfig>
|
||||
</PostHogProvider>
|
||||
</Suspense>
|
||||
</InstanceWrapper>
|
||||
</StoreWrapper>
|
||||
</TranslationProvider>
|
||||
</>
|
||||
</ThemeProvider>
|
||||
</StoreProvider>
|
||||
);
|
||||
}
|
||||
|
||||
+12
-23
@@ -3,7 +3,6 @@ import * as Sentry from "@sentry/react-router";
|
||||
import Script from "next/script";
|
||||
import { Links, Meta, Outlet, Scripts } from "react-router";
|
||||
import type { LinksFunction } from "react-router";
|
||||
import { ThemeProvider, useTheme } from "next-themes";
|
||||
// plane imports
|
||||
import { SITE_DESCRIPTION, SITE_NAME } from "@plane/constants";
|
||||
import { cn } from "@plane/utils";
|
||||
@@ -15,10 +14,9 @@ import faviconIco from "@/app/assets/favicon/favicon.ico?url";
|
||||
import icon180 from "@/app/assets/icons/icon-180x180.png?url";
|
||||
import icon512 from "@/app/assets/icons/icon-512x512.png?url";
|
||||
import ogImage from "@/app/assets/og-image.png?url";
|
||||
import { LogoSpinner } from "@/components/common/logo-spinner";
|
||||
import globalStyles from "@/styles/globals.css?url";
|
||||
import type { Route } from "./+types/root";
|
||||
// components
|
||||
import { LogoSpinner } from "@/components/common/logo-spinner";
|
||||
// local
|
||||
import { CustomErrorComponent } from "./error";
|
||||
import { AppProvider } from "./provider";
|
||||
@@ -53,7 +51,7 @@ export function Layout({ children }: { children: ReactNode }) {
|
||||
const isSessionRecorderEnabled = parseInt(process.env.VITE_ENABLE_SESSION_RECORDER || "0");
|
||||
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
@@ -68,12 +66,16 @@ export function Layout({ children }: { children: ReactNode }) {
|
||||
<Meta />
|
||||
<Links />
|
||||
</head>
|
||||
<body suppressHydrationWarning>
|
||||
<body>
|
||||
<div id="context-menu-portal" />
|
||||
<div id="editor-portal" />
|
||||
<ThemeProvider themes={["light", "dark", "light-contrast", "dark-contrast", "custom"]} defaultTheme="system">
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
<AppProvider>
|
||||
<div
|
||||
className={cn("h-screen w-full overflow-hidden bg-canvas relative flex flex-col", "desktop-app-container")}
|
||||
>
|
||||
<main className="w-full h-full overflow-hidden relative">{children}</main>
|
||||
</div>
|
||||
</AppProvider>
|
||||
<Scripts />
|
||||
{!!isSessionRecorderEnabled && process.env.VITE_SESSION_RECORDER_KEY && (
|
||||
<Script id="clarity-tracking">
|
||||
@@ -116,25 +118,12 @@ export const meta: Route.MetaFunction = () => [
|
||||
];
|
||||
|
||||
export default function Root() {
|
||||
return (
|
||||
<AppProvider>
|
||||
<div className={cn("h-screen w-full overflow-hidden bg-canvas relative flex flex-col", "desktop-app-container")}>
|
||||
<main className="w-full h-full overflow-hidden relative">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
</AppProvider>
|
||||
);
|
||||
return <Outlet />;
|
||||
}
|
||||
|
||||
export function HydrateFallback() {
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
// if we are on the server or the theme is not resolved, return an empty div
|
||||
if (typeof window === "undefined" || resolvedTheme === undefined) return <div />;
|
||||
|
||||
return (
|
||||
<div className="relative flex bg-canvas h-screen w-full items-center justify-center">
|
||||
<div className="relative flex h-screen w-full items-center justify-center">
|
||||
<LogoSpinner />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -276,6 +276,10 @@ export const coreRoutes: RouteConfigEntry[] = [
|
||||
":workspaceSlug/settings/webhooks/:webhookId",
|
||||
"./(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/[webhookId]/page.tsx"
|
||||
),
|
||||
route(
|
||||
":workspaceSlug/settings/api-tokens",
|
||||
"./(all)/[workspaceSlug]/(settings)/settings/(workspace)/api-tokens/page.tsx"
|
||||
),
|
||||
]),
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { observer } from "mobx-react";
|
||||
import { AlertOctagon, BarChart4, CircleDashed, Folder, Microscope } from "lucide-react";
|
||||
import { AlertOctagon, BarChart4, CircleDashed, Folder, Microscope, Search } from "lucide-react";
|
||||
// plane imports
|
||||
import { MARKETING_PRICING_PAGE_LINK } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { getButtonStyling } from "@plane/propel/button";
|
||||
import { SearchIcon } from "@plane/propel/icons";
|
||||
import { ContentWrapper } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
// assets
|
||||
@@ -51,7 +50,7 @@ export const WORKSPACE_ACTIVE_CYCLES_DETAILS = [
|
||||
key: "zoom_into_cycles_that_need_attention",
|
||||
title: "Zoom into cycles that need attention. ",
|
||||
description: "Investigate the state of any cycle that doesn’t conform to expectations in one click.",
|
||||
icon: SearchIcon,
|
||||
icon: Search,
|
||||
},
|
||||
{
|
||||
key: "stay_ahead_of_blockers",
|
||||
|
||||
@@ -12,6 +12,6 @@ export function CommonProjectBreadcrumbs(props: TCommonProjectBreadcrumbProps) {
|
||||
// preferences
|
||||
const { preferences: projectPreferences } = useProjectNavigationPreferences();
|
||||
|
||||
if (projectPreferences.navigationMode === "TABBED") return null;
|
||||
if (projectPreferences.navigationMode === "horizontal") return null;
|
||||
return <ProjectBreadcrumb workspaceSlug={workspaceSlug} projectId={projectId} />;
|
||||
}
|
||||
|
||||
+4
-3
@@ -1,8 +1,9 @@
|
||||
import { Command } from "cmdk";
|
||||
import { observer } from "mobx-react";
|
||||
import { EIconSize } from "@plane/constants";
|
||||
import { Check } from "lucide-react";
|
||||
// plane imports
|
||||
import { CheckIcon, StateGroupIcon } from "@plane/propel/icons";
|
||||
import { EIconSize } from "@plane/constants";
|
||||
import { StateGroupIcon } from "@plane/propel/icons";
|
||||
import { Spinner } from "@plane/ui";
|
||||
// store hooks
|
||||
import { useProjectState } from "@/hooks/store/use-project-state";
|
||||
@@ -35,7 +36,7 @@ export const ChangeWorkItemStateList = observer(function ChangeWorkItemStateList
|
||||
/>
|
||||
<p>{state.name}</p>
|
||||
</div>
|
||||
<div>{state.id === currentStateId && <CheckIcon className="h-3 w-3" />}</div>
|
||||
<div>{state.id === currentStateId && <Check className="h-3 w-3" />}</div>
|
||||
</Command.Item>
|
||||
))
|
||||
) : (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Command } from "cmdk";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { SearchIcon } from "@plane/propel/icons";
|
||||
import { Search } from "lucide-react";
|
||||
// plane imports
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// components
|
||||
import type { TPowerKContext } from "@/components/power-k/core/types";
|
||||
// plane web imports
|
||||
@@ -21,7 +21,7 @@ export function PowerKModalNoSearchResultsCommand(props: TPowerKModalNoSearchRes
|
||||
return (
|
||||
<Command.Group>
|
||||
<PowerKModalCommandItem
|
||||
icon={SearchIcon}
|
||||
icon={Search}
|
||||
value="no-results"
|
||||
label={
|
||||
<p className="flex items-center gap-2">
|
||||
|
||||
@@ -16,7 +16,7 @@ export const ExtendedAppHeader = observer(function ExtendedAppHeader(props: { he
|
||||
// store hooks
|
||||
const { sidebarCollapsed } = useAppTheme();
|
||||
// derived values
|
||||
const shouldShowSidebarToggleButton = projectPreferences.navigationMode === "ACCORDION" || (!projectId && !workItem);
|
||||
const shouldShowSidebarToggleButton = projectPreferences.navigationMode === "accordion" || (!projectId && !workItem);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user