Files
plane/apps/dev-wiki/helpers/string.helper.ts
Bavisetti Narayan 9e11dc9cdc [WIKI-511] feat: inline page comments (#3599)
* feat: page comments

* chore: added node id

* chore: changed reverse relation

* chore: added resolve comment endpoint

* fix: build errors

* fix: basic comment decoration added

* fix: add marks

* fix: operations

* fix: to ditch decorations approach!

* fix: move dev-wiki into apps

* fix: new approach with some more types

* fix: link view container

* fix: editor ee packages updated

* fix: basic threads implementation added

* fix: comments scroll and filtering

* fix scrolling

* chore: added total count for comments

* fix: seperate the comments store

* fix: comments removal

* feat: multi comment on same mark

* fix: mark fixed and styles of comments fixed!

* feat: added better animations

* fix: better styles and animations for comments

* fix: ee seperation

* fix: threads api fix

* fix: new comment store attached

* fix: resolve and delete

* fix: remove framer motion

* fix: add comment ordering

* fix: delete extension

* fix: web

* fix: context aware comments

* fix: new working comment store

* fix: getting changes to web

* fix: ui of comments

* fix: comment mark resolution fixed

* fix: move changes to store

* temp fix: project pages

* fix: comments using lite text editor

* fix: rerendering infinitely

* fix: comment box and reference

* chore: updated the migration file

* fix: move things to ee

* chore: un commented the feature flag logic

* chore: removed an extra line in feature flag

* fix: editor types

* fix: scrolling of thread items

* fix: scrolling of thread items

* fix: type errors

* fix: comment creation

* fix: page comments

* fix: page comments

* fix: remove utils

* fix: added feature flagging

* fix:loading json content

* fix: upload items

* fix: upload items

* fix: ui chanegs

* fix: comment box

* fix: better spacing

* Fix spacing and removing the divs not needed anymore

* fix: comments storing json

* Add comment thread management hooks

* Replace use-new-comment hook with use-scroll-manager

Remove useNewComment hook and related logic. Add useScrollManager hook
for managing scroll behavior in the comments sidebar. Refactor
threads-sidebar to use scroll manager for thread and comment box
navigation. Add cancel button to new comment box.

* Remove hover and click effects for resolved comment marks

* Auto-scroll new comments in sidebar after order sync

Adds highlight and scroll-to behavior for newly created comments.
Sidebar now scrolls to the new thread after comments order updates.

* Add granular page comment permissions and UI checks

Introduce canCurrentUserCommentOnPage permission for pages. Update
comment UI to respect user comment permissions. Refactor comment
creation logic into useNewComment hook. Hide comment/reply UI for users
without permission.

* Refactor comments components and imports for clarity and type safety

* Refactor comments components to use local types and remove unused files

- Move type definitions from shared types file into local components -
Remove unused comment-reactions component and types/index.ts - Update
imports to use local hooks and types - Minor code cleanup and formatting

* Refactor comments feature flag and clean up comments components

- Replace isPageCommentsEnabled with isCommentsEnabled in page stores -
Remove unused types, utils, and reactions components from comments -
Move and rename comments components for consistency - Update editor
flagging to use new comments feature flag logic - Simplify comments
sidebar and thread item structure - Remove redundant exports and props
from comments modules

* Refactor comments extension to use workspace feature flag

- Pass storeType to navigation pane extensions - Check isCommentsEnabled
with workspaceSlug before rendering comments - Update canComment logic
to use canCurrentUserCommentOnPage - Remove console.log and unused code
- Fix types for extension props

* Add canCurrentUserCommentOnPage to TeamspacePage

* Refactor TCommentConfig type and move EditorSideEffects

- Move TCommentConfig to ce/types/editor.ts and ee

* fix: ce sync

* fix: prosemirror-model fixed

* chore: removed the extra migration

* chore: added page comments serializer

* fix: comments

* refactor: comments

* fix: ce/ee seperation

* chore: changed the page serialization

* fix: page comments refactored

* fix: remove ce changes

* fix: restoration for nested pages fixed

* fix: less ce changes

* fix: imports

* fix: better refactoring of page root and editor body

* fix: renaming files

* fix: editor sideeffects

* fix: add comments json field

* fix: page comments types

* fix: lint warnings

* fix: props name change

* fix: props name

* fix: comment mark

* fix: import type

* fix: review changes

* fix: extra checks

* fix: type changes

* fix: remove editor ref current

* fix: renaming

* fix: renaming files

* fix: comments extension revamp

* fix: declaring better types

* fix: better types

* fix: hooks seperated

* fix: edit box

* fix: html validation

* fix: agents md

* fix: naming convention

* fix: update names and service methods

* fix: more fixes

* fix: attributs

* fix: dev wiki sync

---------

Co-authored-by: Palanikannan M <akashmalinimurugu@gmail.com>
Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2025-09-01 20:56:52 +05:30

255 lines
7.6 KiB
TypeScript

import DOMPurify from "isomorphic-dompurify";
export const addSpaceIfCamelCase = (str: string) => {
if (str === undefined || str === null) return "";
if (typeof str !== "string") str = `${str}`;
return str.replace(/([a-z])([A-Z])/g, "$1 $2");
};
export const replaceUnderscoreIfSnakeCase = (str: string) => str.replace(/_/g, " ");
export const capitalizeFirstLetter = (str: string) => str.charAt(0).toUpperCase() + str.slice(1);
export const truncateText = (str: string, length: number) => {
if (!str || str === "") return "";
return str.length > length ? `${str.substring(0, length)}...` : str;
};
export const createSimilarString = (str: string) => {
const shuffled = str
.split("")
.sort(() => Math.random() - 0.5)
.join("");
return shuffled;
};
const fallbackCopyTextToClipboard = (text: string) => {
const textArea = document.createElement("textarea");
textArea.value = text;
// Avoid scrolling to bottom
textArea.style.top = "0";
textArea.style.left = "0";
textArea.style.position = "fixed";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
// FIXME: Even though we are using this as a fallback, execCommand is deprecated 👎. We should find a better way to do this.
// https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand
document.execCommand("copy");
} catch (err) {
// catch fallback error
}
document.body.removeChild(textArea);
};
export const copyTextToClipboard = async (text: string) => {
if (!navigator.clipboard) {
fallbackCopyTextToClipboard(text);
return;
}
await navigator.clipboard.writeText(text);
};
export const generateRandomColor = (string: string): string => {
if (!string) return "rgb(var(--color-primary-100))";
string = `${string}`;
const uniqueId = string.length.toString() + string; // Unique identifier based on string length
const combinedString = uniqueId + string;
const hash = Array.from(combinedString).reduce((acc, char) => {
const charCode = char.charCodeAt(0);
return (acc << 5) - acc + charCode;
}, 0);
const hue = hash % 360;
const saturation = 70; // Higher saturation for pastel colors
const lightness = 60; // Mid-range lightness for pastel colors
const randomColor = `hsl(${hue}, ${saturation}%, ${lightness}%)`;
return randomColor;
};
export const getFirstCharacters = (str: string) => {
const words = str.trim().split(" ");
if (words.length === 1) {
return words[0].charAt(0);
} else {
return words[0].charAt(0) + words[1].charAt(0);
}
};
/**
* @description: This function will remove all the HTML tags from the string
* @param {string} html
* @return {string}
* @example:
* const html = "<p>Some text</p>";
* const text = stripHTML(html);
* console.log(text); // Some text
*/
export const sanitizeHTML = (htmlString: string) => {
const sanitizedText = DOMPurify.sanitize(htmlString, { ALLOWED_TAGS: [] }); // sanitize the string to remove all HTML tags
return sanitizedText.trim(); // trim the string to remove leading and trailing whitespaces
};
/**
*
* @example:
* const html = "<p>Some text</p>";
* const text = stripAndTruncateHTML(html);
* console.log(text); // Some text
*/
export const stripAndTruncateHTML = (html: string, length: number = 55) => truncateText(sanitizeHTML(html), length);
/**
* @description: This function return number count in string if number is more than 100 then it will return 99+
* @param {number} number
* @return {string}
* @example:
* const number = 100;
* const text = getNumberCount(number);
* console.log(text); // 99+
*/
export const getNumberCount = (number: number): string => {
if (number > 99) {
return "99+";
}
return number.toString();
};
export const objToQueryParams = (obj: any) => {
const params = new URLSearchParams();
if (!obj) return params.toString();
for (const [key, value] of Object.entries(obj)) {
if (value !== undefined && value !== null) params.append(key, value as string);
}
return params.toString();
};
/**
* @returns {boolean} true if searchQuery is substring of text in the same order, false otherwise
* @description Returns true if searchQuery is substring of text in the same order, false otherwise
* @param {string} text string to compare from
* @param {string} searchQuery
* @example substringMatch("hello world", "hlo") => true
* @example substringMatch("hello world", "hoe") => false
*/
export const substringMatch = (text: string, searchQuery: string): boolean => {
try {
let searchIndex = 0;
for (let i = 0; i < text.length; i++) {
if (text[i].toLowerCase() === searchQuery[searchIndex]?.toLowerCase()) searchIndex++;
// All characters of searchQuery found in order
if (searchIndex === searchQuery.length) return true;
}
// Not all characters of searchQuery found in order
return false;
} catch (error) {
return false;
}
};
/**
* @returns {boolean} true if email is valid, false otherwise
* @description Returns true if email is valid, false otherwise
* @param {string} email string to check if it is a valid email
* @example checkEmailIsValid("hello world") => false
* @example checkEmailIsValid("example@plane.so") => true
*/
export const checkEmailValidity = (email: string): boolean => {
if (!email) return false;
const isEmailValid =
/^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(
email
);
return isEmailValid;
};
export const isEmptyHtmlString = (htmlString: string, allowedHTMLTags: string[] = []) => {
// Remove HTML tags using regex
const cleanText = DOMPurify.sanitize(htmlString, { ALLOWED_TAGS: allowedHTMLTags });
// Trim the string and check if it's empty
return cleanText.trim() === "";
};
/**
* @description this function returns whether a comment is empty or not by checking for the following conditions-
* 1. If editor content is undefined
* 2. If editor content is an empty string
* 3. If editor content is "<p></p>"
* @param {string | undefined} comment
* @returns {boolean}
*/
export const isEditorContentEmpty = (content: string | undefined): boolean => {
// return true if editor content is undefined
if (!content) return true;
return (
content?.trim() === "" ||
content === "<p></p>" ||
isEmptyHtmlString(content ?? "", ["img", "mention-component", "image-component", "issue-embed-component"])
);
};
/**
* @description this function returns whether a comment is empty or not by checking for the following conditions-
* 1. If comment is undefined
* 2. If comment is an empty string
* 3. If comment is "<p></p>"
* @param {string | undefined} comment
* @returns {boolean}
*/
export const isCommentEmpty = (comment: string | undefined): boolean => {
// return true if comment is undefined
if (!comment) return true;
return (
comment?.trim() === "" ||
comment === "<p></p>" ||
isEmptyHtmlString(comment ?? "", ["img", "mention-component", "image-component", "embed-component"])
);
};
/**
* @description
* This function test whether a URL is valid or not.
*
* It accepts URLs with or without the protocol.
* @param {string} url
* @returns {boolean}
* @example
* checkURLValidity("https://example.com") => true
* checkURLValidity("example.com") => true
* checkURLValidity("example") => false
*/
export const checkURLValidity = (url: string): boolean => {
if (!url) return false;
// regex to support complex query parameters and fragments
const urlPattern =
/^(https?:\/\/)?((([a-z\d-]+\.)*[a-z\d-]+\.[a-z]{2,6})|(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}))(:\d+)?(\/[\w.-]*)*(\?[^#\s]*)?(#[\w-]*)?$/i;
return urlPattern.test(url);
};