Compare commits

...
Author SHA1 Message Date
Aaryan Khandelwal 546198f3b6 refactor: remove extensions enum from editor package 2026-01-16 19:22:12 +05:30
SangeethaandGitHub fa1b4a102a [WEB-5890] migration: added getting_started_checklist, tips, explored_feature fields on the workspace member table (#8489)
* migration: added getting_started_checklist and tips field

* fix: remove defaults and added explored_features field

* fix: added user table migration
2026-01-15 14:25:31 +05:30
sriramveeraghanta 8399f64bee chore(deps): react router upgraded 2026-01-09 14:43:36 +05:30
Bavisetti NarayanandGitHub 5f3f9d2623 chore: updated migration file name (#8515) 2026-01-09 14:36:45 +05:30
Prateek ShouryaandGitHub 110dbd9acd [VPAT-50] chore(security): add X-Frame-Options header to nginx configuration to prevent clickjacking attacks (#8507)
* [VPAT-50] chore(security): add X-Frame-Options header to nginx configuration to prevent clickjacking attacks

* [SECURITY] chore: enhance nginx configuration with additional security headers
2026-01-09 04:03:41 +05:30
Vipin ChaudharyandGitHub b83d460938 [WIKI-826] chore: add unique id as key to logo selector (#8494) 2026-01-07 15:05:14 +05:30
ea1f92e0c6 [WEB-5537]refactor: rename IssueUserProperty to ProjectUserProperty and update related references (#8206)
* refactor: rename IssueUserProperty to ProjectUserProperty and update related references across the codebase

* migrate: move issue user properties to project user properties and update related fields and constraints

* refactor: rename IssueUserPropertySerializer and IssueUserDisplayPropertyEndpoint to ProjectUserPropertySerializer and ProjectUserDisplayPropertyEndpoint, updating all related references

* fix: enhance ProjectUserDisplayPropertyEndpoint to handle missing properties by creating new entries and improve response handling

* fix: correct formatting in migration for ProjectUserProperty model options

* migrate: add migration to update existing non-service API tokens to remove workspace association

* migrate: refine migration to update existing non-service API tokens by excluding bot users from workspace removal

* chore: changed the project sort order in project user property

* chore: remove allowed_rate_limit from APIToken

* chore: updated user-properties endpoint for frontend

* chore: removed the extra projectuserproperty

* chore: updated the migration file

* chore: code refactor

* fix: type error

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: sangeethailango <sangeethailango21@gmail.com>
Co-authored-by: vamsikrishnamathala <matalav55@gmail.com>
Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia@plane.so>
2026-01-06 15:37:19 +05:30
Prateek ShouryaandGitHub 3d5e427894 chore: remove unused right sidebar component and clean up workspace member settings (#8477) 2025-12-31 00:08:40 +05:30
sriram veeraghantaandGitHub 6129e8e53b Revert "[WEB-4959]chore: refactor project member page #8464" (#8476)
This reverts commit c97e418515.
2025-12-30 21:46:32 +05:30
sriram veeraghantaandGitHub d61b157929 chore: remove posthog events (#8465)
* chore: remove posthog events

* chore: remove event tracking

* chore: lint errors

* chore: minor changes based on comments

* fix: type errors
2025-12-30 19:59:50 +05:30
NikhilandGitHub a37e5e2f6a [WEB-5598] refactor: streamline object creation in workspace seed task and improve error handling in workspace creation #8264 2025-12-30 17:28:33 +05:30
lifandGitHub a976bea5af fix: correct spelling error in database.ts log message (#8452)
Fix "convertion" -> "conversion" in error log message.

Signed-off-by: majiayu000 <1835304752@qq.com>
2025-12-30 17:23:42 +05:30
1072509642 migration: added webhook version, navigation related fields and allowed_rate_limit for APIToken (#8339)
* migration: added version field in webhook

* chore: add max_length

* chore: added product tour fields

* chore: updated the migration file

* chore: removed the duplicated migration file

* chore: added allowed_rate_limit for api_tokens

* chore: changed key feature tour to product tour

* chore: added is_subscribed_to_changelog field

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2025-12-30 16:18:58 +05:30
Jayash TripathyandGitHub dcd8d27eae [WEB-5472] refactor: project form #8472 2025-12-30 15:57:33 +05:30
b-saikrishnakanthandGitHub c97e418515 [WEB-4959]chore: refactor project member page #8464 2025-12-30 14:59:16 +05:30
Aaryan KhandelwalandGitHub 031baaa162 [WEB-857] regression: image uploader error state #8471 2025-12-30 14:35:52 +05:30
Jayash TripathyandGitHub 9141a9377f [WEB-5472] refactor: components of project creation flow (#8462) 2025-12-30 14:32:48 +05:30
Anmol Singh BhatiaandGitHub 866338289e chore: navigation preference enhancements (#8468) 2025-12-30 13:22:28 +05:30
Prateek ShouryaandGitHub 8d479ac24c [WEB-5822] fix: migrate ImagePickerPopover to Propel Tabs component and render only enabled tabs #8290
- Replace custom tab implementation with Propel Tabs
- Dynamically render only enabled tabs based on configuration
- Filter tabs by isEnabled property for cleaner conditional rendering
- Improve tab navigation and accessibility with Propel components
2025-12-29 19:54:32 +05:30
lifandGitHub 94d5779f3a chore: remove unused get_client_ip import (#8453)
Remove unused import `get_client_ip` from workspace/invite.py.
Identified by ruff linter (F401 error).

Signed-off-by: majiayu000 <1835304752@qq.com>
2025-12-29 18:39:54 +05:30
b-saikrishnakanthandGitHub 20492ffa75 [WEB-5790] feat: new email templates (#8423) 2025-12-29 16:17:29 +05:30
Anmol Singh BhatiaandGitHub 630d2b9600 [WEB-5179] chore: icon utils code refactor #8458 2025-12-29 13:31:27 +05:30
Shaikh NaasirandGitHub ec215bb3b1 Typo: database extension error message (#8461) 2025-12-29 13:26:09 +05:30
Bavisetti NarayanandGitHub 3241693d6d [WEB-5815] chore: removed the deleted states (#8457) 2025-12-26 20:28:33 +05:30
Aaryan KhandelwalandGitHub 6cc5c1946a regression: red and green color backgrounds (#8456) 2025-12-26 19:17:03 +05:30
Anmol Singh BhatiaandGitHub 2980c2d76b refactor: actions icon migration (#8219)
* chore: gitignore updated

* chore: check icon added to propel package

* feat: search icon migration

* chore: check icon migration

* chore: plus icon added to propel package

* chore: code refactor

* chore: plus icon migration and code refactor

* chore: trash icon added to propel package

* chore: code refactor

* chore: trash icon migration

* chore: edit icon added to propel package

* chore: new tab icon added to propel package

* chore: edit icon migration

* chore: newtab icon migration

* chore: lock icon added to propel package

* chore: lock icon migration

* chore: globe icon added to propel package

* chore: globe icon migration

* chore: copy icon added to propel package

* chore: copy icon migration

* chore: link icon added to propel package

* chore: link icon migration

* chore: link icon migration

* chore: info icon added to propel package

* chore: code refactor

* chore: code refactor

* chore: code refactor

* chore: code refactor
2025-12-26 17:19:15 +05:30
Aaryan KhandelwalandGitHub 92ac28fcb8 [WEB-5809] refactor: tailwind config inline variables (#8437) 2025-12-26 16:36:29 +05:30
Vamsi KrishnaandGitHub e3ba7c2889 [WEB-5781]chore: removed info banner for preferences #8442 2025-12-26 13:40:15 +05:30
Vamsi KrishnaandGitHub 78eeb9ce0d [WEB-5785]fix: favorites icon size #8449 2025-12-26 13:38:46 +05:30
Prateek ShouryaandGitHub f04be48f61 [WEB-5804] refactor: decouple filter value types from filter configurations (#8441)
* [WEB-5804] refactor: decouple filter value types from filter configurations

Remove value type constraints from filter configurations to support
operator-specific value types. Different operators can accept different
value types for the same filter property, so value types should be
determined at the operator level rather than the filter level.

- Remove generic value type parameter from TFilterConfig
- Update TOperatorConfigMap to accept union of all value types
- Simplify filter config factory signatures across all filter types
- Add forceUpdate parameter to updateConditionValue method

* refactor: remove filter value type constraints from filter configurations

Eliminate the generic value type parameter from filter configurations to allow for operator-specific value types. This change enhances flexibility by enabling different operators to accept various value types for the same filter property.

- Updated TFilterConfig and related interfaces to remove value type constraints
- Adjusted filter configuration methods and types accordingly
- Refactored date operator support to align with the new structure
2025-12-24 21:03:22 +05:30
Vamsi KrishnaandGitHub 5499e49b72 [WEB-5574]chore: notification card refactor (#8234)
* chore: notification card refactor

* chore: moved base activity types to constants package
2025-12-24 20:32:50 +05:30
Prateek ShouryaandGitHub 3c8624b1ba [WEB-5324] refactor: add Unified OAuth Configuration and Missing Gitea Options (#8050)
* refactor: add Unified OAuth Configuration and Missing Gitea Options

- Replaced the AuthenticationModes component with a more streamlined implementation using AuthenticationMethodCard.
- Removed obsolete authentication modes files from the codebase.
- Enhanced the AuthRoot component to utilize the new OAuth configuration hook for better management of authentication options.
- Updated type definitions for instance authentication modes to reflect the new structure.

* refactor: update OAuth type imports and remove obsolete types

- Replaced local type imports with centralized imports from @plane/types in core, extended, and index OAuth hooks.
- Removed the now redundant types.ts file as its definitions have been migrated.
- Enhanced type definitions for OAuth options to improve consistency across the application.

* feat: add new Gitea logo and update OAuth icon imports to use standard HTML img tags

* chore: remove unused authentication logos and upgrade button component
2025-12-24 20:20:12 +05:30
Anmol Singh BhatiaandGitHub 55c49cc633 [WEB-5808] chore: sidebar project list enhancements (#8451)
* chore: sidebar project list enhancements

* chore: code refactor

* chore: code refactor
2025-12-24 19:53:00 +05:30
Vamsi KrishnaandGitHub 5b28327551 [WEB-5782]chore: migrated modals to @plane/ui (#8420)
* chore: migrated modal to @plane/ui

* chore: fixed spacings
2025-12-24 19:45:55 +05:30
39728d4cc4 [WEB-5779] fix: handle loading state while fetching project cover image (#8419)
* refactor: replace cover image handling with CoverImage component across profile and project forms

* fix: extend CoverImage component to accept additional img props

* Update apps/web/core/components/common/cover-image.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix: handle undefined cover image URL in ProfileSidebar component

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-24 19:30:01 +05:30
Bavisetti NarayanandGitHub 59f26a80bb [WEB-5667] fix: estimate value display in analytics #8448 2025-12-24 17:34:58 +05:30
SangeethaandGitHub 1a7c537ee5 [WEB-5791] fix: broken favicon in links (#8396)
* fix: using base url of a redirect url

* chore: internal networks check for the final_url

* fix: none final_url

* fix: exception handling

* fix: exception handling

* chore: remove unused imports

* refactor: moved ip address check logic into separate function

* fix: ValueError logic
2025-12-24 17:33:46 +05:30
Prateek ShouryaandGitHub 27a7cdcfc3 [WEB-5581] fix: resolve logo spinner hydration and theme loading issues (#8450)
- Fix hydration mismatch by lazy loading components that depend on theme
- Ensure LogoSpinner renders with correct theme on initial load
2025-12-24 17:29:27 +05:30
619 changed files with 7019 additions and 11089 deletions
@@ -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.&nbsp;
<span className="text-danger">Misconfigs can lead to email bounces and errors.</span>
<span className="text-danger-primary">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 { ExternalLink, HelpCircle, MoveLeft } from "lucide-react";
import { HelpCircle, MoveLeft } from "lucide-react";
import { Transition } from "@headlessui/react";
// plane internal packages
import { WEB_BASE_URL } from "@plane/constants";
import { DiscordIcon, GithubIcon, PageIcon } from "@plane/propel/icons";
// plane internal packages
import { DiscordIcon, GithubIcon, NewTabIcon, 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`}
>
<ExternalLink size={16} />
<NewTabIcon width={14} height={14} />
{!isSidebarCollapsed && "Redirect to Plane"}
</a>
</Tooltip>
@@ -118,7 +118,7 @@ export function WorkspaceCreateForm() {
/>
)}
/>
<span className="text-11 text-red-500">{errors?.name?.message}</span>
<span className="text-11 text-danger-primary">{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-red-500">This URL is taken. Try something else.</p>}
{slugError && <p className="text-13 text-danger-primary">This URL is taken. Try something else.</p>}
{invalidSlug && (
<p className="text-13 text-red-500">{`URLs can contain only ( - ), ( _ ) and alphanumeric characters.`}</p>
<p className="text-13 text-danger-primary">{`URLs can contain only ( - ), ( _ ) and alphanumeric characters.`}</p>
)}
{errors.slug && <span className="text-11 text-red-500">{errors.slug.message}</span>}
{errors.slug && <span className="text-11 text-danger-primary">{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-red-500">{errors.organization_size.message}</span>
<span className="text-13 text-danger-primary">{errors.organization_size.message}</span>
)}
</div>
</div>
+2 -2
View File
@@ -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-red-500">*</span>
Email <span className="text-danger-primary">*</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-red-500">*</span>
Password <span className="text-danger-primary">*</span>
</label>
<div className="relative">
<Input
-11
View File
@@ -1,11 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 702 B

-17
View File
@@ -1,17 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 2.0 KiB

+6 -4
View File
@@ -10,20 +10,22 @@ export function Banner(props: TBanner) {
return (
<div
className={`rounded-md p-2 w-full border ${type === "error" ? "bg-red-500/5 border-red-400" : "bg-green-500/5 border-green-400"}`}
className={`rounded-md p-2 w-full border ${type === "error" ? "bg-danger-subtle border-danger-strong" : "bg-success-subtle border-success-strong"}`}
>
<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-red-600" aria-hidden="true" />
<AlertCircle className="h-5 w-5 text-danger-primary" aria-hidden="true" />
</span>
) : (
<CheckCircle2 className="h-5 w-5 text-green-600" aria-hidden="true" />
<CheckCircle2 className="h-5 w-5 text-success-primary" aria-hidden="true" />
)}
</div>
<div className="ml-1">
<p className={`text-13 font-medium ${type === "error" ? "text-red-600" : "text-green-600"}`}>{message}</p>
<p className={`text-13 font-medium ${type === "error" ? "text-danger-primary" : "text-success-primary"}`}>
{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>
<Copy size={18} color="#B9B9B9" />
<CopyIcon width={18} height={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-red-500">*</span>
First name <span className="text-danger-primary">*</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-red-500">*</span>
Last name <span className="text-danger-primary">*</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-red-500">*</span>
Email <span className="text-danger-primary">*</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-red-500">{errorData.message}</p>
<p className="px-1 text-11 text-danger-primary">{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-red-500">*</span>
Company name <span className="text-danger-primary">*</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-red-500">*</span>
Set a password <span className="text-danger-primary">*</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-red-500">{errorData.message}</p>
<p className="px-1 text-11 text-danger-primary">{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-red-500">*</span>
Confirm password <span className="text-danger-primary">*</span>
</label>
<div className="relative">
<Input
@@ -311,7 +311,9 @@ export function InstanceSetupForm() {
</div>
{!!formData.confirm_password &&
formData.password !== formData.confirm_password &&
renderPasswordMatchError && <span className="text-13 text-red-500">Passwords don{"'"}t match</span>}
renderPasswordMatchError && (
<span className="text-13 text-danger-primary">Passwords don{"'"}t match</span>
)}
</div>
<div className="relative flex gap-2">
@@ -1,7 +1,8 @@
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
@@ -75,7 +76,7 @@ export const WorkspaceListItem = observer(function WorkspaceListItem({ workspace
</div>
</div>
<div className="flex-shrink-0">
<ExternalLink size={16} className="text-placeholder group-hover:text-secondary" />
<NewTabIcon width={14} height={16} className="text-placeholder group-hover:text-secondary" />
</div>
</a>
);
@@ -1,6 +1,6 @@
import { Image, BrainCog, Cog, Lock, Mail } from "lucide-react";
import { Image, BrainCog, Cog, Mail } from "lucide-react";
// plane imports
import { WorkspaceIcon } from "@plane/propel/icons";
import { LockIcon, 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: Lock,
Icon: LockIcon,
name: "Authentication",
description: "Configure authentication modes.",
href: `/authentication/`,
+6
View File
@@ -20,6 +20,12 @@ 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;
-1
View File
@@ -45,7 +45,6 @@
"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 -8
View File
@@ -18,7 +18,7 @@ from drf_spectacular.utils import OpenApiResponse, OpenApiRequest
from plane.db.models import (
Cycle,
Intake,
IssueUserProperty,
ProjectUserProperty,
Module,
Project,
DeployBoard,
@@ -218,8 +218,6 @@ 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
@@ -229,11 +227,6 @@ 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(
[
+1 -1
View File
@@ -52,7 +52,7 @@ from .issue import (
IssueCreateSerializer,
IssueActivitySerializer,
IssueCommentSerializer,
IssueUserPropertySerializer,
ProjectUserPropertySerializer,
IssueAssigneeSerializer,
LabelSerializer,
IssueSerializer,
+3 -3
View File
@@ -18,7 +18,7 @@ from plane.db.models import (
Issue,
IssueActivity,
IssueComment,
IssueUserProperty,
ProjectUserProperty,
IssueAssignee,
IssueSubscriber,
IssueLabel,
@@ -346,9 +346,9 @@ class IssueActivitySerializer(BaseSerializer):
fields = "__all__"
class IssueUserPropertySerializer(BaseSerializer):
class ProjectUserPropertySerializer(BaseSerializer):
class Meta:
model = IssueUserProperty
model = ProjectUserProperty
fields = "__all__"
read_only_fields = ["user", "workspace", "project"]
+4 -4
View File
@@ -14,7 +14,7 @@ from plane.app.views import (
IssueReactionViewSet,
IssueRelationViewSet,
IssueSubscriberViewSet,
IssueUserDisplayPropertyEndpoint,
ProjectUserDisplayPropertyEndpoint,
IssueViewSet,
LabelViewSet,
BulkArchiveIssuesEndpoint,
@@ -208,13 +208,13 @@ urlpatterns = [
name="project-issue-comment-reactions",
),
## End Comment Reactions
## IssueUserProperty
## ProjectUserProperty
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-properties/",
IssueUserDisplayPropertyEndpoint.as_view(),
ProjectUserDisplayPropertyEndpoint.as_view(),
name="project-issue-display-properties",
),
## IssueUserProperty End
## ProjectUserProperty End
## Issue Archives
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-issues/",
+1 -1
View File
@@ -114,7 +114,7 @@ from .asset.v2 import (
from .issue.base import (
IssueListEndpoint,
IssueViewSet,
IssueUserDisplayPropertyEndpoint,
ProjectUserDisplayPropertyEndpoint,
BulkDeleteIssuesEndpoint,
DeletedIssuesListViewSet,
IssuePaginatedViewSet,
+23 -13
View File
@@ -34,7 +34,7 @@ from plane.app.serializers import (
IssueDetailSerializer,
IssueListDetailSerializer,
IssueSerializer,
IssueUserPropertySerializer,
ProjectUserPropertySerializer,
)
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,
IssueUserProperty,
ProjectUserProperty,
ModuleIssue,
Project,
ProjectMember,
@@ -723,23 +723,33 @@ class IssueViewSet(BaseViewSet):
return Response(status=status.HTTP_204_NO_CONTENT)
class IssueUserDisplayPropertyEndpoint(BaseAPIView):
class ProjectUserDisplayPropertyEndpoint(BaseAPIView):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def patch(self, request, slug, project_id):
issue_property = IssueUserProperty.objects.get(user=request.user, project_id=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.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)
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)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def get(self, request, slug, project_id):
issue_property, _ = IssueUserProperty.objects.get_or_create(user=request.user, project_id=project_id)
serializer = IssueUserPropertySerializer(issue_property)
issue_property, _ = ProjectUserProperty.objects.get_or_create(user=request.user, project_id=project_id)
serializer = ProjectUserPropertySerializer(issue_property)
return Response(serializer.data, status=status.HTTP_200_OK)
+2 -8
View File
@@ -24,14 +24,15 @@ 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,
)
@@ -250,8 +251,6 @@ 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
@@ -261,11 +260,6 @@ 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(
[
+4 -4
View File
@@ -24,7 +24,7 @@ from plane.db.models import (
User,
WorkspaceMember,
Project,
IssueUserProperty,
ProjectUserProperty,
)
from plane.db.models.project import ProjectNetwork
from plane.utils.host import base_host
@@ -160,9 +160,9 @@ class UserProjectInvitationsViewset(BaseViewSet):
ignore_conflicts=True,
)
IssueUserProperty.objects.bulk_create(
ProjectUserProperty.objects.bulk_create(
[
IssueUserProperty(
ProjectUserProperty(
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(
workspace_id=project_invite.workspace_id,
project_id=project_id,
member=user,
role=project_invite.role,
)
+16 -16
View File
@@ -1,6 +1,7 @@
# 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
@@ -13,7 +14,7 @@ from plane.app.serializers import (
from plane.app.permissions import WorkspaceUserPermission
from plane.db.models import Project, ProjectMember, IssueUserProperty, WorkspaceMember
from plane.db.models import Project, ProjectMember, ProjectUserProperty, 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
@@ -89,24 +90,23 @@ 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 list of project members of the requested workspace with the given slug
project_members = (
ProjectMember.objects.filter(
# Get the minimum sort_order for each member in the workspace
member_sort_orders = (
ProjectUserProperty.objects.filter(
workspace__slug=slug,
member_id__in=[member.get("member_id") for member in members],
user_id__in=[member.get("member_id") for member in members],
)
.values("member_id", "sort_order")
.order_by("sort_order")
.values("user_id")
.annotate(min_sort_order=Min("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:
# 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"))
]
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)
# 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(
IssueUserProperty(
ProjectUserProperty(
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)
_ = IssueUserProperty.objects.bulk_create(bulk_issue_props, batch_size=10, ignore_conflicts=True)
_ = ProjectUserProperty.objects.bulk_create(bulk_issue_props, batch_size=10, ignore_conflicts=True)
project_members = ProjectMember.objects.filter(
project_id=project_id,
+6 -8
View File
@@ -77,14 +77,12 @@ 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,7 +26,6 @@ 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
+38 -15
View File
@@ -1,7 +1,6 @@
# Python imports
import logging
# Third party imports
from celery import shared_task
import requests
@@ -20,6 +19,34 @@ 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.
@@ -31,17 +58,6 @@ 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
@@ -49,9 +65,16 @@ 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(url, headers=headers, timeout=1)
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)
soup = BeautifulSoup(response.content, "html.parser")
title_tag = soup.find("title")
@@ -60,8 +83,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
favicon_base64 = fetch_and_encode_favicon(headers, soup, url)
# Fetch and encode favicon using final URL (after redirects)
favicon_base64 = fetch_and_encode_favicon(headers, soup, final_url)
# Prepare result
result = {
+31 -14
View File
@@ -21,7 +21,7 @@ from plane.db.models import (
WorkspaceMember,
Project,
ProjectMember,
IssueUserProperty,
ProjectUserProperty,
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.objects.create(
project = Project(
**project_seed,
workspace=workspace,
name=workspace.name, # Use workspace name
@@ -105,6 +105,7 @@ 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(
@@ -121,9 +122,9 @@ def create_project_and_member(workspace: Workspace, bot_user: User) -> Dict[int,
)
# Create issue user properties
IssueUserProperty.objects.bulk_create(
ProjectUserProperty.objects.bulk_create(
[
IssueUserProperty(
ProjectUserProperty(
project=project,
user_id=workspace_member["member_id"],
workspace_id=workspace.id,
@@ -191,13 +192,13 @@ def create_project_states(
state_id = state_seed.pop("id")
project_id = state_seed.pop("project_id")
state = State.objects.create(
state = State(
**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
@@ -224,12 +225,13 @@ 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.objects.create(
label = Label(
**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")
@@ -276,13 +278,14 @@ def create_project_issues(
cycle_id = issue_seed.pop("cycle_id")
module_ids = issue_seed.pop("module_ids")
issue = Issue.objects.create(
issue = Issue(
**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],
@@ -351,7 +354,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.objects.create(
page = Page(
workspace_id=workspace.id,
is_global=False,
access=page_seed.get("access", Page.PUBLIC_ACCESS),
@@ -365,16 +368,18 @@ 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":
ProjectPage.objects.create(
project_page = ProjectPage(
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
@@ -414,7 +419,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.objects.create(
cycle = Cycle(
**cycle_seed,
start_date=start_date,
end_date=end_date,
@@ -423,6 +428,7 @@ 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")
@@ -450,7 +456,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.objects.create(
module = Module(
**module_seed,
start_date=start_date,
target_date=end_date,
@@ -458,6 +464,7 @@ 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
@@ -478,13 +485,15 @@ 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")
IssueView.objects.create(
view_seed.pop("id")
issue_view = IssueView(
**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
@@ -518,6 +527,14 @@ 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,
IssueUserProperty,
ProjectUserProperty,
)
@@ -47,27 +47,18 @@ 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, sort_order=sort_order, role=role
is_active=True, role=role
)
else:
# Create the project member
ProjectMember.objects.create(project=project, member=user, role=role, sort_order=sort_order)
ProjectMember.objects.create(project=project, member=user, role=role)
# Issue Property
IssueUserProperty.objects.get_or_create(user=user, project=project)
ProjectUserProperty.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}"))
@@ -0,0 +1,57 @@
# 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),
]
@@ -0,0 +1,50 @@
# 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'),
),
]
@@ -0,0 +1,51 @@
# 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),
]
@@ -0,0 +1,38 @@
# 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),
),
]
+1 -1
View File
@@ -34,7 +34,6 @@ from .issue import (
IssueLabel,
IssueLink,
IssueMention,
IssueUserProperty,
IssueReaction,
IssueRelation,
IssueSequence,
@@ -54,6 +53,7 @@ from .project import (
ProjectMemberInvite,
ProjectNetwork,
ProjectPublicMember,
ProjectUserProperty,
)
from .session import Session
from .social_connection import SocialLoginConnection
+1
View File
@@ -32,6 +32,7 @@ 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"
-38
View File
@@ -90,14 +90,6 @@ 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)
@@ -534,36 +526,6 @@ 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")
+47 -8
View File
@@ -12,7 +12,6 @@ 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"))
@@ -219,14 +218,20 @@ class ProjectMember(ProjectBaseModel):
is_active = models.BooleanField(default=True)
def save(self, *args, **kwargs):
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"]
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")
# Project ordering
if smallest_sort_order is not None:
self.sort_order = smallest_sort_order - 10000
# 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),
)
super(ProjectMember, self).save(*args, **kwargs)
@@ -326,3 +331,37 @@ 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)
+3 -3
View File
@@ -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(models.Manager):
class StateManager(SoftDeletionManager):
"""Default manager - excludes triage states"""
def get_queryset(self):
return super().get_queryset().exclude(group=StateGroup.TRIAGE.value)
class TriageStateManager(models.Manager):
class TriageStateManager(SoftDeletionManager):
"""Manager for triage states only"""
def get_queryset(self):
+12 -2
View File
@@ -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,6 +192,10 @@ 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"),
@@ -221,7 +225,9 @@ 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)
@@ -233,8 +239,12 @@ 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"
+1
View File
@@ -38,6 +38,7 @@ 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}"
+14
View File
@@ -112,6 +112,16 @@ 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))
@@ -204,6 +214,9 @@ 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"]
@@ -325,6 +338,7 @@ 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,
IssueUserProperty,
ProjectUserProperty,
State,
WorkspaceMember,
User,
@@ -82,8 +82,8 @@ class TestProjectAPIPost(TestProjectBase):
assert project_member.role == 20 # Administrator
assert project_member.is_active is True
# Verify IssueUserProperty was created
assert IssueUserProperty.objects.filter(project=project, user=user).exists()
# Verify ProjectUserProperty was created
assert ProjectUserProperty.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 IssueUserProperty
assert IssueUserProperty.objects.filter(project=project).count() == 2
# Verify both have ProjectUserProperty
assert ProjectUserProperty.objects.filter(project=project).count() == 2
@pytest.mark.django_db
def test_create_project_guest_forbidden(self, session_client, workspace):
+1 -1
View File
@@ -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__value", "estimate_point__key", None),
"ESTIMATE_POINTS": ("estimate_point__key", "estimate_point__value", None),
"CYCLES": (
"issue_cycle__cycle_id",
"issue_cycle__cycle__name",
@@ -1,330 +1,306 @@
<!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" >
<!DOCTYPE html>
<html lang="en">
<head>
<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>
<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>
</head>
<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
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 doesnt 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 werent 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>
</html>
+261 -284
View File
@@ -1,288 +1,265 @@
<!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" >
<!DOCTYPE html>
<html lang="en">
<head>
<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>
<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>
</head>
<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
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>
</html>
@@ -1,2 +1,215 @@
<!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>
<!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. Weve 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>
@@ -1,219 +1,306 @@
<!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">
<!DOCTYPE html>
<html lang="en">
<head>
<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>
<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>
</head>
<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
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 doesnt 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>
</html>
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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 convertion from html:", error);
logger.error("Failed to save binary after first conversion 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-red-500` : `border-subtle`
!isFocused && Boolean(emailError?.email) ? `border-danger-strong` : `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-red-500 border-0 focus:bg-none active:bg-transparent`}
className={`disable-autofill-style h-10 w-full placeholder:text-placeholder autofill:bg-danger-subtle 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-red-600 px-0.5">
<p className="flex items-center gap-1 text-11 text-danger-primary 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-red-500">Passwords don{"'"}t match</span>}
renderPasswordMatchError && <span className="text-13 text-danger-primary">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-green-700">
<p className="flex items-center gap-1 font-medium text-success-primary">
<CircleCheck height={12} width={12} />
Paste the code sent to your email
</p>
@@ -1,6 +1,6 @@
import React from "react";
// lucide icons
import { Check } from "lucide-react";
// plane imports
import { CheckIcon } from "@plane/propel/icons";
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 && <Check size={10} strokeWidth={3} />}
{isChecked && <CheckIcon width={10} height={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,7 +1,6 @@
import React, { useState } from "react";
import { observer } from "mobx-react";
import { Search } from "lucide-react";
import { CloseIcon } from "@plane/propel/icons";
import { SearchIcon, CloseIcon } from "@plane/propel/icons";
// types
import type { IIssueFilterOptions, TIssueFilterKeys } from "@/types/issue";
// local imports
@@ -25,7 +24,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">
<Search className="text-placeholder" size={12} strokeWidth={2} />
<SearchIcon className="text-placeholder" width={12} height={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 { Link, Paperclip } from "lucide-react";
import { ViewsIcon } from "@plane/propel/icons";
import { Paperclip } from "lucide-react";
import { LinkIcon, 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">
<Link className="h-3 w-3 flex-shrink-0" strokeWidth={2} />
<LinkIcon 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-red-500": shouldHighLight && due_date && shouldHighlightIssueDueDate(due_date, state?.group),
"text-danger-primary": 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-red-500" />
<LogOut size={12} className="shrink-0 text-danger-primary" />
<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 { Check, MessageSquare, MoreVertical } from "lucide-react";
import { MessageSquare, MoreVertical } from "lucide-react";
import { Menu, Transition } from "@headlessui/react";
// plane imports
import type { EditorRefApi } from "@plane/editor";
import { CloseIcon } from "@plane/propel/icons";
import { CheckIcon, 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-green-500 bg-green-500/20 p-2 shadow-md duration-300 hover:bg-green-500"
className="group rounded-sm border border-success-strong bg-success-primary p-2 shadow-md duration-300 hover:bg-success-primary"
>
<Check className="h-3 w-3 text-green-500 duration-300 group-hover:text-on-color" strokeWidth={2} />
<CheckIcon className="h-3 w-3 text-on-color" strokeWidth={2} />
</button>
<button
type="button"
className="group rounded-sm border border-red-500 bg-red-500/20 p-2 shadow-md duration-300 hover:bg-red-500"
className="group rounded-sm border border-danger-strong bg-danger-primary p-2 shadow-md duration-300 hover:bg-danger-primary-hover"
onClick={() => setIsEditing(false)}
>
<CloseIcon className="h-3 w-3 text-red-500 duration-300 group-hover:text-on-color" strokeWidth={2} />
<CloseIcon className="h-3 w-3 text-on-color" strokeWidth={2} />
</button>
</div>
</form>
@@ -1,9 +1,9 @@
import React from "react";
import { observer } from "mobx-react";
import { Link2, MoveRight } from "lucide-react";
import { MoveRight } from "lucide-react";
import { Listbox, Transition } from "@headlessui/react";
// ui
import { CenterPanelIcon, FullScreenPanelIcon, SidePanelIcon } from "@plane/propel/icons";
import { LinkIcon, 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}
>
<Link2 className="h-4 w-4 -rotate-45" />
<LinkIcon 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-red-500": shouldHighlightIssueDueDate(issueDetails.target_date, state?.group),
"text-danger-primary": 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 bg-opacity-50 transition-opacity" />
<div className="fixed inset-0 z-20 bg-backdrop 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-red-600 text-red-600": isDownVotedByUser,
"border-danger-strong text-danger-primary": isDownVotedByUser,
"border-strong": !isDownVotedByUser,
"cursor-default": isInIframe,
}
-1
View File
@@ -50,7 +50,6 @@
"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, PROJECT_TRACKER_ELEMENTS } from "@plane/constants";
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { EmptyStateDetailed } from "@plane/propel/empty-state";
import { Tabs } from "@plane/propel/tabs";
@@ -11,7 +11,6 @@ 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";
@@ -120,7 +119,6 @@ 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 === "horizontal" && (
{projectPreferences.navigationMode === "TABBED" && (
<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);
}}
>
<Plus className="size-3" />
<PlusIcon 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">
<Search className="h-3.5 w-3.5 text-placeholder" />
<SearchIcon 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")}
@@ -23,7 +23,7 @@ function ProjectLayout({ params }: Route.ComponentProps) {
return (
<>
{projectPreferences.navigationMode === "horizontal" && (
{projectPreferences.navigationMode === "TABBED" && (
<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,7 +2,7 @@ import { useState } from "react";
import { observer } from "mobx-react";
import { useParams, useRouter, useSearchParams } from "next/navigation";
// constants
import { EPageAccess, PROJECT_PAGE_TRACKER_EVENTS, PROJECT_TRACKER_ELEMENTS } from "@plane/constants";
import { EPageAccess } from "@plane/constants";
// plane types
import { Button } from "@plane/propel/button";
import { PageIcon } from "@plane/propel/icons";
@@ -12,7 +12,6 @@ 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
@@ -40,23 +39,10 @@ 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!",
@@ -86,13 +72,7 @@ export const PagesListHeader = observer(function PagesListHeader() {
</Header.LeftItem>
{canCurrentUserCreatePage && (
<Header.RightItem>
<Button
variant="primary"
size="lg"
onClick={handleCreatePage}
loading={isCreatingPage}
data-ph-element={PROJECT_TRACKER_ELEMENTS.CREATE_HEADER_BUTTON}
>
<Button variant="primary" size="lg" onClick={handleCreatePage} loading={isCreatingPage}>
{isCreatingPage ? "Adding" : "Add page"}
</Button>
</Header.RightItem>
@@ -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 { ViewsIcon } from "@plane/propel/icons";
import { LockIcon, 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"}>
<Lock className="h-4 w-4" />
<LockIcon className="h-4 w-4" />
</Tooltip>
</div>
) : (
@@ -1,12 +1,9 @@
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
@@ -18,17 +15,6 @@ 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">
<Search className="text-secondary" size={14} strokeWidth={2} />
<SearchIcon className="text-secondary" width={14} height={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}
@@ -8,8 +8,6 @@ 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
@@ -48,7 +46,6 @@ 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>
@@ -1,15 +1,10 @@
import { useState } from "react";
import { observer } from "mobx-react";
import { Search } from "lucide-react";
// types
import {
EUserPermissions,
EUserPermissionsLevel,
MEMBER_TRACKER_ELEMENTS,
MEMBER_TRACKER_EVENTS,
} from "@plane/constants";
import { EUserPermissions, EUserPermissionsLevel } 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";
@@ -20,8 +15,6 @@ 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";
@@ -42,7 +35,7 @@ const WorkspaceMembersSettingsPage = observer(function WorkspaceMembersSettingsP
const {
workspace: { workspaceMemberIds, inviteMembersToWorkspace, filtersStore },
} = useMember();
const { currentWorkspace, mutateWorkspaceMembersActivity } = useWorkspace();
const { currentWorkspace } = useWorkspace();
const { t } = useTranslation();
// derived values
@@ -55,17 +48,9 @@ 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!",
@@ -77,14 +62,6 @@ 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!",
@@ -137,7 +114,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">
<Search className="h-3.5 w-3.5 text-placeholder" />
<SearchIcon 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")}...`}
@@ -154,12 +131,7 @@ const WorkspaceMembersSettingsPage = observer(function WorkspaceMembersSettingsP
/>
<MembersActivityButton workspaceSlug={workspaceSlug} />
{canPerformWorkspaceAdminActions && (
<Button
variant="primary"
size="lg"
onClick={() => setInviteModal(true)}
data-ph-element={MEMBER_TRACKER_ELEMENTS.HEADER_ADD_BUTTON}
>
<Button variant="primary" size="lg" onClick={() => setInviteModal(true)}>
{t("workspace_settings.settings.members.add_member")}
</Button>
)}
@@ -1,7 +1,7 @@
import { useState } from "react";
import { observer } from "mobx-react";
import useSWR from "swr";
import { EUserPermissions, EUserPermissionsLevel, WORKSPACE_SETTINGS_TRACKER_EVENTS } from "@plane/constants";
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { IWebhook } from "@plane/types";
// ui
@@ -11,7 +11,6 @@ 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";
@@ -55,12 +54,6 @@ 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!",
@@ -68,12 +61,6 @@ 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!",
@@ -2,7 +2,7 @@ import { useEffect, 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 { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// components
import { EmptyStateCompact } from "@plane/propel/empty-state";
@@ -13,7 +13,6 @@ 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";
@@ -72,9 +71,6 @@ 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);
},
}}
@@ -94,9 +90,6 @@ 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);
},
},
@@ -1,8 +1,7 @@
import React, { useState } from "react";
import { useState } 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";
@@ -14,7 +13,6 @@ 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();
@@ -51,9 +49,6 @@ function ApiTokensPage() {
button={{
label: t("workspace_settings.settings.api_tokens.add_token"),
onClick: () => {
captureClick({
elementName: PROFILE_SETTINGS_TRACKER_ELEMENTS.HEADER_ADD_PAT_BUTTON,
});
setIsCreateTokenModalOpen(true);
},
}}
@@ -72,9 +67,6 @@ function ApiTokensPage() {
button={{
label: t("workspace_settings.settings.api_tokens.add_token"),
onClick: () => {
captureClick({
elementName: PROFILE_SETTINGS_TRACKER_ELEMENTS.HEADER_ADD_PAT_BUTTON,
});
setIsCreateTokenModalOpen(true);
},
}}
@@ -89,9 +81,6 @@ function ApiTokensPage() {
{
label: t("settings_empty_state.tokens.cta_primary"),
onClick: () => {
captureClick({
elementName: PROFILE_SETTINGS_TRACKER_ELEMENTS.EMPTY_STATE_ADD_PAT_BUTTON,
});
setIsCreateTokenModalOpen(true);
},
},
@@ -159,7 +159,9 @@ function SecurityPage() {
/>
)}
</div>
{errors.old_password && <span className="text-11 text-red-500">{errors.old_password.message}</span>}
{errors.old_password && (
<span className="text-11 text-danger-primary">{errors.old_password.message}</span>
)}
</div>
)}
<div className="space-y-1">
@@ -199,7 +201,9 @@ function SecurityPage() {
</div>
{passwordSupport}
{isNewPasswordSameAsOldPassword && !isPasswordInputFocused && (
<span className="text-11 text-red-500">{t("new_password_must_be_different_from_old_password")}</span>
<span className="text-11 text-danger-primary">
{t("new_password_must_be_different_from_old_password")}
</span>
)}
</div>
<div className="space-y-1">
@@ -238,7 +242,7 @@ function SecurityPage() {
)}
</div>
{!!confirmPassword && password !== confirmPassword && renderPasswordMatchError && (
<span className="text-13 text-red-500">{t("auth.common.password.errors.match")}</span>
<span className="text-13 text-danger-primary">{t("auth.common.password.errors.match")}</span>
)}
</div>
</div>
@@ -1,8 +1,9 @@
import { observer } from "mobx-react";
import { useParams, usePathname } from "next/navigation";
import { CircleUser, Activity, Bell, CircleUserRound, KeyRound, Settings2, Blocks, Lock } from "lucide-react";
import { CircleUser, Activity, Bell, CircleUserRound, KeyRound, Settings2, Blocks } 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";
@@ -11,7 +12,7 @@ import { useUser } from "@/hooks/store/user";
const ICONS = {
profile: CircleUser,
security: Lock,
security: LockIcon,
activity: Activity,
preferences: Settings2,
notifications: Bell,
+3 -25
View File
@@ -1,11 +1,11 @@
import React, { useState } from "react";
import { 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, MEMBER_TRACKER_EVENTS, MEMBER_TRACKER_ELEMENTS, GROUP_WORKSPACE_TRACKER_EVENT } from "@plane/constants";
import { ROLE } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// types
import { Button } from "@plane/propel/button";
@@ -19,9 +19,7 @@ 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";
@@ -79,18 +77,6 @@ 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);
@@ -107,14 +93,7 @@ function UserInvitationsPage() {
setIsJoiningWorkspaces(false);
});
})
.catch((err) => {
captureError({
eventName: MEMBER_TRACKER_EVENTS.accept,
payload: {
member_id: invitationsRespond?.[0],
},
error: err,
});
.catch((_err) => {
setToast({
type: TOAST_TYPE.ERROR,
title: t("error"),
@@ -183,7 +162,6 @@ 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>
+7 -3
View File
@@ -157,7 +157,9 @@ function SecurityPage() {
/>
)}
</div>
{errors.old_password && <span className="text-11 text-red-500">{errors.old_password.message}</span>}
{errors.old_password && (
<span className="text-11 text-danger-primary">{errors.old_password.message}</span>
)}
</div>
)}
<div className="space-y-1">
@@ -197,7 +199,9 @@ function SecurityPage() {
</div>
{passwordSupport}
{isNewPasswordSameAsOldPassword && !isPasswordInputFocused && (
<span className="text-11 text-red-500">{t("new_password_must_be_different_from_old_password")}</span>
<span className="text-11 text-danger-primary">
{t("new_password_must_be_different_from_old_password")}
</span>
)}
</div>
<div className="space-y-1">
@@ -236,7 +240,7 @@ function SecurityPage() {
)}
</div>
{!!confirmPassword && password !== confirmPassword && renderPasswordMatchError && (
<span className="text-13 text-red-500">{t("auth.common.password.errors.match")}</span>
<span className="text-13 text-danger-primary">{t("auth.common.password.errors.match")}</span>
)}
</div>
</div>
+1 -1
View File
@@ -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-red-500"
className="flex items-center justify-center gap-2 text-13 font-medium text-danger-primary"
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, Check, Share2, Star, User2 } from "lucide-react";
import { CloseIcon } from "@plane/propel/icons";
import { Boxes, Share2, Star, User2 } from "lucide-react";
import { CheckIcon, 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={Check} title="Accept" action={handleAccept} />
<EmptySpaceItem Icon={CheckIcon} title="Accept" action={handleAccept} />
<EmptySpaceItem Icon={CloseIcon} title="Ignore" action={handleReject} />
</EmptySpace>
)
+2 -2
View File
@@ -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-red-500 mb-2">
<h2 className="text-20 font-semibold text-danger-primary 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-red-500 mb-2">Error</h2>
<h2 className="text-20 font-semibold text-danger-primary mb-2">Error</h2>
<div className="h-px w-full bg-subtle-1" />
</div>
+13 -18
View File
@@ -1,5 +1,5 @@
import { lazy, Suspense } from "react";
import { useTheme, ThemeProvider } from "next-themes";
import { useTheme } 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 PostHogProvider = lazy(function PostHogProvider() {
return import("@/lib/posthog-provider");
const InstanceWrapper = lazy(function InstanceWrapper() {
return import("@/lib/wrappers/instance-wrapper");
});
const ChatSupportModal = lazy(function ChatSupportModal() {
@@ -33,32 +33,27 @@ 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>
<ToastWithTheme />
<Toast theme={resolveGeneralTheme(resolvedTheme)} />
<StoreWrapper>
<InstanceWrapper>
<Suspense>
<ChatSupportModal />
<PostHogProvider>
<SWRConfig value={WEB_SWR_CONFIG}>{children}</SWRConfig>
</PostHogProvider>
<SWRConfig value={WEB_SWR_CONFIG}>{children}</SWRConfig>
</Suspense>
</InstanceWrapper>
</StoreWrapper>
</TranslationProvider>
</ThemeProvider>
</>
</StoreProvider>
);
}
+23 -12
View File
@@ -3,6 +3,7 @@ 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";
@@ -14,9 +15,10 @@ 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";
@@ -51,7 +53,7 @@ export function Layout({ children }: { children: ReactNode }) {
const isSessionRecorderEnabled = parseInt(process.env.VITE_ENABLE_SESSION_RECORDER || "0");
return (
<html lang="en">
<html lang="en" suppressHydrationWarning>
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
@@ -66,16 +68,12 @@ export function Layout({ children }: { children: ReactNode }) {
<Meta />
<Links />
</head>
<body>
<body suppressHydrationWarning>
<div id="context-menu-portal" />
<div id="editor-portal" />
<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>
<ThemeProvider themes={["light", "dark", "light-contrast", "dark-contrast", "custom"]} defaultTheme="system">
{children}
</ThemeProvider>
<Scripts />
{!!isSessionRecorderEnabled && process.env.VITE_SESSION_RECORDER_KEY && (
<Script id="clarity-tracking">
@@ -118,12 +116,25 @@ export const meta: Route.MetaFunction = () => [
];
export default function Root() {
return <Outlet />;
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>
);
}
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 h-screen w-full items-center justify-center">
<div className="relative flex bg-canvas h-screen w-full items-center justify-center">
<LogoSpinner />
</div>
);
@@ -1,9 +1,10 @@
import { observer } from "mobx-react";
import { AlertOctagon, BarChart4, CircleDashed, Folder, Microscope, Search } from "lucide-react";
import { AlertOctagon, BarChart4, CircleDashed, Folder, Microscope } 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
@@ -50,7 +51,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 doesnt conform to expectations in one click.",
icon: Search,
icon: SearchIcon,
},
{
key: "stay_ahead_of_blockers",
@@ -12,6 +12,6 @@ export function CommonProjectBreadcrumbs(props: TCommonProjectBreadcrumbProps) {
// preferences
const { preferences: projectPreferences } = useProjectNavigationPreferences();
if (projectPreferences.navigationMode === "horizontal") return null;
if (projectPreferences.navigationMode === "TABBED") return null;
return <ProjectBreadcrumb workspaceSlug={workspaceSlug} projectId={projectId} />;
}
@@ -1,9 +1,8 @@
import { Command } from "cmdk";
import { observer } from "mobx-react";
import { Check } from "lucide-react";
// plane imports
import { EIconSize } from "@plane/constants";
import { StateGroupIcon } from "@plane/propel/icons";
// plane imports
import { CheckIcon, StateGroupIcon } from "@plane/propel/icons";
import { Spinner } from "@plane/ui";
// store hooks
import { useProjectState } from "@/hooks/store/use-project-state";
@@ -36,7 +35,7 @@ export const ChangeWorkItemStateList = observer(function ChangeWorkItemStateList
/>
<p>{state.name}</p>
</div>
<div>{state.id === currentStateId && <Check className="h-3 w-3" />}</div>
<div>{state.id === currentStateId && <CheckIcon className="h-3 w-3" />}</div>
</Command.Item>
))
) : (
@@ -1,7 +1,7 @@
import { Command } from "cmdk";
import { Search } from "lucide-react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { SearchIcon } from "@plane/propel/icons";
// plane imports
// 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={Search}
icon={SearchIcon}
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 (
<>
@@ -1,6 +1,6 @@
import { observer } from "mobx-react";
import { Trash } from "lucide-react";
import { PROJECT_SETTINGS_TRACKER_ELEMENTS } from "@plane/constants";
import { TrashIcon } from "@plane/propel/icons";
type TEstimateListItem = {
estimateId: string;
@@ -22,7 +22,7 @@ export const EstimateListItemButtons = observer(function EstimateListItemButtons
onClick={() => onDeleteClick && onDeleteClick(estimateId)}
data-ph-element={PROJECT_SETTINGS_TRACKER_ELEMENTS.ESTIMATES_LIST_ITEM}
>
<Trash size={12} />
<TrashIcon width={12} height={12} />
</button>
</div>
);
+3 -3
View File
@@ -1,7 +1,7 @@
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// icons
import { Circle, ExternalLink } from "lucide-react";
import { Circle } from "lucide-react";
// plane imports
import {
EUserPermissions,
@@ -12,7 +12,7 @@ import {
} from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button";
import { WorkItemsIcon } from "@plane/propel/icons";
import { NewTabIcon, WorkItemsIcon } from "@plane/propel/icons";
import { Tooltip } from "@plane/propel/tooltip";
import { EIssuesStoreType } from "@plane/types";
import { Breadcrumbs, Header } from "@plane/ui";
@@ -95,7 +95,7 @@ export const IssuesHeader = observer(function IssuesHeader() {
>
<Circle className="h-1.5 w-1.5 fill-accent-primary" strokeWidth={2} />
{t("workspace_projects.network.public.title")}
<ExternalLink className="hidden h-3 w-3 group-hover:block" strokeWidth={2} />
<NewTabIcon className="hidden h-3 w-3 group-hover:block" strokeWidth={2} />
</a>
) : (
<></>
@@ -1,11 +1,11 @@
import type { Copy } from "lucide-react";
import type { ISvgIcons } from "@plane/propel/icons";
import type { TContextMenuItem } from "@plane/ui";
export interface CopyMenuHelperProps {
baseItem: {
key: string;
title: string;
icon: typeof Copy;
icon: React.FC<ISvgIcons>;
action: () => void;
shouldRender: boolean;
};
@@ -1,9 +1,10 @@
import type { FC } from "react";
import { CalendarDays, LayersIcon, Link2, Paperclip } from "lucide-react";
import { CalendarDays, LayersIcon, Paperclip } from "lucide-react";
// types
import { ISSUE_GROUP_BY_OPTIONS } from "@plane/constants";
import type { ISvgIcons } from "@plane/propel/icons";
import {
LinkIcon,
CycleIcon,
StatePropertyIcon,
ModuleIcon,
@@ -82,7 +83,7 @@ export const SpreadSheetPropertyIconMap: Record<string, FC<ISvgIcons>> = {
PriorityPropertyIcon: PriorityPropertyIcon,
StartDatePropertyIcon: StartDatePropertyIcon,
StatePropertyIcon: StatePropertyIcon,
Link2: Link2,
Link2: LinkIcon,
Paperclip: Paperclip,
LayersIcon: LayersIcon,
};
@@ -63,7 +63,7 @@ export const TopNavigationRoot = observer(function TopNavigationRoot() {
<div className="relative">
<InboxIcon className="size-5" />
{totalNotifications > 0 && (
<span className="absolute -top-0 -right-0 size-2 rounded-full bg-red-500" />
<span className="absolute top-0 right-0 size-2 rounded-full bg-danger-primary" />
)}
</div>
),
@@ -10,8 +10,6 @@ import IssuesTour from "@/app/assets/onboarding/issues.webp?url";
import ModulesTour from "@/app/assets/onboarding/modules.webp?url";
import PagesTour from "@/app/assets/onboarding/pages.webp?url";
import ViewsTour from "@/app/assets/onboarding/views.webp?url";
// helpers
import { captureClick } from "@/helpers/event-tracker.helper";
// hooks
import { useCommandPalette } from "@/hooks/store/use-command-palette";
import { useUser } from "@/hooks/store/user";
@@ -107,9 +105,6 @@ export const TourRoot = observer(function TourRoot(props: TOnboardingTourProps)
<Button
variant="primary"
onClick={() => {
captureClick({
elementName: PRODUCT_TOUR_TRACKER_ELEMENTS.START_BUTTON,
});
setStep("work-items");
}}
>
@@ -119,9 +114,6 @@ export const TourRoot = observer(function TourRoot(props: TOnboardingTourProps)
type="button"
className="bg-transparent text-11 font-medium text-accent-primary outline-subtle-1"
onClick={() => {
captureClick({
elementName: PRODUCT_TOUR_TRACKER_ELEMENTS.SKIP_BUTTON,
});
onComplete();
}}
>
@@ -170,9 +162,6 @@ export const TourRoot = observer(function TourRoot(props: TOnboardingTourProps)
<Button
variant="primary"
onClick={() => {
captureClick({
elementName: PRODUCT_TOUR_TRACKER_ELEMENTS.CREATE_PROJECT_BUTTON,
});
onComplete();
toggleCreateProjectModal(true);
}}
@@ -2,10 +2,7 @@ import { useState, useEffect, useRef } from "react";
import { observer } from "mobx-react";
import { LockKeyhole, LockKeyholeOpen } from "lucide-react";
// plane imports
import { PROJECT_PAGE_TRACKER_ELEMENTS } from "@plane/constants";
import { Tooltip } from "@plane/propel/tooltip";
// helpers
import { captureClick } from "@/helpers/event-tracker.helper";
// hooks
import { usePageOperations } from "@/hooks/use-page-operations";
// store
@@ -79,7 +76,6 @@ export const PageLockControl = observer(function PageLockControl({ page }: Props
<button
type="button"
onClick={toggleLock}
data-ph-element={PROJECT_PAGE_TRACKER_ELEMENTS.LOCK_BUTTON}
className="flex-shrink-0 size-6 grid place-items-center rounded-sm text-secondary hover:text-primary hover:bg-layer-1 transition-colors"
aria-label="Lock"
>
@@ -92,7 +88,6 @@ export const PageLockControl = observer(function PageLockControl({ page }: Props
<button
type="button"
onClick={toggleLock}
data-ph-element={PROJECT_PAGE_TRACKER_ELEMENTS.LOCK_BUTTON}
className="h-6 flex items-center gap-1 px-2 rounded-sm text-accent-primary bg-accent-primary/20 hover:bg-accent-primary/30 transition-colors"
aria-label="Locked"
>
@@ -1,4 +1,3 @@
import type { FC } from "react";
import { Controller, useFormContext } from "react-hook-form";
// plane imports
import { NETWORK_CHOICES, ETabIndices } from "@plane/constants";
@@ -79,7 +78,7 @@ function ProjectAttributes(props: Props) {
placeholder={t("lead")}
multiple={false}
buttonVariant="border-with-text"
tabIndex={5}
tabIndex={getIndex("lead")}
/>
</div>
);
@@ -1,24 +1,21 @@
import { useState } from "react";
import { observer } from "mobx-react";
import { FormProvider, useForm } from "react-hook-form";
import { PROJECT_TRACKER_EVENTS, RANDOM_EMOJI_CODES } from "@plane/constants";
// plane imports
import { useTranslation } from "@plane/i18n";
// ui
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { EFileAssetType } from "@plane/types";
import type { IProject } from "@plane/types";
// constants
// components
import ProjectCommonAttributes from "@/components/project/create/common-attributes";
import ProjectCreateHeader from "@/components/project/create/header";
import ProjectCreateButtons from "@/components/project/create/project-create-buttons";
// hooks
import { DEFAULT_COVER_IMAGE_URL, getCoverImageType, uploadCoverImage } from "@/helpers/cover-image.helper";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { getCoverImageType, uploadCoverImage } from "@/helpers/cover-image.helper";
import { useProject } from "@/hooks/store/use-project";
import { usePlatformOS } from "@/hooks/use-platform-os";
// plane web types
import type { TProject } from "@/plane-web/types/projects";
import ProjectAttributes from "./attributes";
import { ProjectAttributes } from "./attributes";
import { getProjectFormValues } from "./utils";
export type TCreateProjectFormProps = {
@@ -37,7 +34,7 @@ export const CreateProjectForm = observer(function CreateProjectForm(props: TCre
const { t } = useTranslation();
const { addProjectToFavorites, createProject, updateProject } = useProject();
// states
const [isChangeInIdentifierRequired, setIsChangeInIdentifierRequired] = useState(true);
const [shouldAutoSyncIdentifier, setShouldAutoSyncIdentifier] = useState(true);
// form info
const methods = useForm<TProject>({
defaultValues: { ...getProjectFormValues(), ...data },
@@ -98,12 +95,6 @@ export const CreateProjectForm = observer(function CreateProjectForm(props: TCre
await updateCoverImageStatus(res.id, coverImage);
await updateProject(workspaceSlug.toString(), res.id, { cover_image_url: coverImage });
}
captureSuccess({
eventName: PROJECT_TRACKER_EVENTS.create,
payload: {
identifier: formData.identifier,
},
});
setToast({
type: TOAST_TYPE.SUCCESS,
title: t("success"),
@@ -117,13 +108,6 @@ export const CreateProjectForm = observer(function CreateProjectForm(props: TCre
})
.catch((err) => {
try {
captureError({
eventName: PROJECT_TRACKER_EVENTS.create,
payload: {
identifier: formData.identifier,
},
});
// Handle the new error format where codes are nested in arrays under field names
const errorData = err?.data ?? {};
@@ -167,7 +151,7 @@ export const CreateProjectForm = observer(function CreateProjectForm(props: TCre
const handleClose = () => {
onClose();
setIsChangeInIdentifierRequired(true);
setShouldAutoSyncIdentifier(true);
setTimeout(() => {
reset();
}, 300);
@@ -182,8 +166,8 @@ export const CreateProjectForm = observer(function CreateProjectForm(props: TCre
<ProjectCommonAttributes
setValue={setValue}
isMobile={isMobile}
isChangeInIdentifierRequired={isChangeInIdentifierRequired}
setIsChangeInIdentifierRequired={setIsChangeInIdentifierRequired}
shouldAutoSyncIdentifier={shouldAutoSyncIdentifier}
setShouldAutoSyncIdentifier={setShouldAutoSyncIdentifier}
/>
<ProjectAttributes isMobile={isMobile} />
</div>
@@ -1,11 +1,6 @@
type TProjectTemplateDropdownSize = "xs" | "sm";
export type TProjectTemplateSelect = {
disabled?: boolean;
size?: TProjectTemplateDropdownSize;
placeholder?: string;
dropDownContainerClassName?: string;
handleModalClose: () => void;
onClick?: () => void;
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
+1 -1
View File
@@ -23,7 +23,7 @@ export const ISSUE_RELATION_OPTIONS: Record<TIssueRelationTypes, TRelationObject
blocked_by: {
key: "blocked_by",
i18n_label: "issue.relation.blocked_by",
className: "bg-red-500/20 text-red-700",
className: "bg-danger-subtle text-danger-primary",
icon: (size) => <CircleDot size={size} className="text-secondary" />,
placeholder: "None",
},
@@ -1,6 +1,6 @@
import { observer } from "mobx-react";
import { Check } from "lucide-react";
import { Combobox } from "@headlessui/react";
import { CheckIcon } from "@plane/propel/icons";
import { cn } from "@plane/utils";
export type TStateOptionProps = {
@@ -31,7 +31,7 @@ export const StateOption = observer(function StateOption(props: TStateOptionProp
{({ selected }) => (
<>
<span className="flex-grow truncate">{option.content}</span>
{selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />}
{selected && <CheckIcon className="h-3.5 w-3.5 flex-shrink-0" />}
</>
)}
</Combobox.Option>

Some files were not shown because too many files have changed in this diff Show More