[WEB-3096] feat: stickies page (#6380)
* feat: added independent stickies page * chore: randomized sticky color * chore: search in stickies * feat: dnd * fix: quick links * fix: stickies abrupt rendering * fix: handled edge cases for dnd * fix: empty states * fix: build and lint * fix: handled new sticky when last sticky is emoty * fix: new sticky condition * refactor: stickies empty states, store * chore: update stickies empty states * fix: random sticky color * fix: header * refactor: better error handling --------- Co-authored-by: NarayanBavisetti <narayan3119@gmail.com> Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
This commit is contained in:
parent
d2c9b437f4
commit
fd7eedc343
56 changed files with 1347 additions and 574 deletions
|
|
@ -1,45 +1,50 @@
|
|||
import { orderBy, set } from "lodash";
|
||||
import { observable, action, makeObservable, runInAction } from "mobx";
|
||||
import { computedFn } from "mobx-utils";
|
||||
import { TSticky } from "@plane/types";
|
||||
import { STICKIES_PER_PAGE } from "@plane/constants";
|
||||
import { InstructionType, TLoader, TPaginationInfo, TSticky } from "@plane/types";
|
||||
import { StickyService } from "@/services/sticky.service";
|
||||
|
||||
export interface IStickyStore {
|
||||
creatingSticky: boolean;
|
||||
fetchingWorkspaceStickies: boolean;
|
||||
loader: TLoader;
|
||||
workspaceStickies: Record<string, string[]>; // workspaceId -> stickyIds
|
||||
stickies: Record<string, TSticky>; // stickyId -> sticky
|
||||
searchQuery: string;
|
||||
activeStickyId: string | undefined;
|
||||
recentStickyId: string | undefined;
|
||||
showAddNewSticky: boolean;
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
|
||||
paginationInfo: TPaginationInfo | undefined;
|
||||
// computed
|
||||
getWorkspaceStickies: (workspaceSlug: string) => string[];
|
||||
|
||||
getWorkspaceStickyIds: (workspaceSlug: string) => string[];
|
||||
// actions
|
||||
toggleShowNewSticky: (value: boolean) => void;
|
||||
updateSearchQuery: (query: string) => void;
|
||||
fetchWorkspaceStickies: (workspaceSlug: string, cursor?: string, per_page?: number) => void;
|
||||
createSticky: (workspaceSlug: string, sticky: Partial<TSticky>) => void;
|
||||
updateSticky: (workspaceSlug: string, id: string, updates: Partial<TSticky>) => void;
|
||||
deleteSticky: (workspaceSlug: string, id: string) => void;
|
||||
fetchWorkspaceStickies: (workspaceSlug: string) => void;
|
||||
createSticky: (workspaceSlug: string, sticky: Partial<TSticky>) => Promise<void>;
|
||||
updateSticky: (workspaceSlug: string, id: string, updates: Partial<TSticky>) => Promise<void>;
|
||||
deleteSticky: (workspaceSlug: string, id: string) => Promise<void>;
|
||||
updateActiveStickyId: (id: string | undefined) => void;
|
||||
fetchRecentSticky: (workspaceSlug: string) => void;
|
||||
incrementPage: () => void;
|
||||
fetchRecentSticky: (workspaceSlug: string) => Promise<void>;
|
||||
fetchNextWorkspaceStickies: (workspaceSlug: string) => Promise<void>;
|
||||
updateStickyPosition: (
|
||||
workspaceSlug: string,
|
||||
stickyId: string,
|
||||
destinationId: string,
|
||||
edge: InstructionType
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
export class StickyStore implements IStickyStore {
|
||||
loader: TLoader = "init-loader";
|
||||
creatingSticky = false;
|
||||
fetchingWorkspaceStickies = true;
|
||||
workspaceStickies: Record<string, string[]> = {};
|
||||
stickies: Record<string, TSticky> = {};
|
||||
recentStickyId: string | undefined = undefined;
|
||||
searchQuery = "";
|
||||
activeStickyId: string | undefined = undefined;
|
||||
showAddNewSticky = false;
|
||||
currentPage = 0;
|
||||
totalPages = 0;
|
||||
paginationInfo: TPaginationInfo | undefined = undefined;
|
||||
|
||||
// services
|
||||
stickyService;
|
||||
|
|
@ -48,33 +53,35 @@ export class StickyStore implements IStickyStore {
|
|||
makeObservable(this, {
|
||||
// observables
|
||||
creatingSticky: observable,
|
||||
fetchingWorkspaceStickies: observable,
|
||||
loader: observable,
|
||||
activeStickyId: observable,
|
||||
showAddNewSticky: observable,
|
||||
recentStickyId: observable,
|
||||
workspaceStickies: observable,
|
||||
stickies: observable,
|
||||
searchQuery: observable,
|
||||
currentPage: observable,
|
||||
totalPages: observable,
|
||||
// actions
|
||||
updateSearchQuery: action,
|
||||
updateSticky: action,
|
||||
deleteSticky: action,
|
||||
incrementPage: action,
|
||||
fetchNextWorkspaceStickies: action,
|
||||
fetchWorkspaceStickies: action,
|
||||
createSticky: action,
|
||||
updateActiveStickyId: action,
|
||||
toggleShowNewSticky: action,
|
||||
fetchRecentSticky: action,
|
||||
updateStickyPosition: action,
|
||||
});
|
||||
this.stickyService = new StickyService();
|
||||
}
|
||||
|
||||
getWorkspaceStickies = computedFn((workspaceSlug: string) => {
|
||||
let filteredStickies = (this.workspaceStickies[workspaceSlug] || []).map((stickyId) => this.stickies[stickyId]);
|
||||
if (this.searchQuery) {
|
||||
filteredStickies = filteredStickies.filter(
|
||||
(sticky) => sticky.name && sticky.name.toLowerCase().includes(this.searchQuery.toLowerCase())
|
||||
);
|
||||
}
|
||||
return filteredStickies.map((sticky) => sticky.id);
|
||||
});
|
||||
getWorkspaceStickyIds = computedFn((workspaceSlug: string) =>
|
||||
orderBy(
|
||||
(this.workspaceStickies[workspaceSlug] || []).map((stickyId) => this.stickies[stickyId]),
|
||||
["sort_order"],
|
||||
["desc"]
|
||||
).map((sticky) => sticky.id)
|
||||
);
|
||||
|
||||
toggleShowNewSticky = (value: boolean) => {
|
||||
this.showAddNewSticky = value;
|
||||
|
|
@ -88,34 +95,77 @@ export class StickyStore implements IStickyStore {
|
|||
this.activeStickyId = id;
|
||||
};
|
||||
|
||||
incrementPage = () => {
|
||||
this.currentPage += 1;
|
||||
};
|
||||
|
||||
fetchRecentSticky = async (workspaceSlug: string) => {
|
||||
const response = await this.stickyService.getStickies(workspaceSlug, "1:0:0", 1);
|
||||
const response = await this.stickyService.getStickies(workspaceSlug, "1:0:0");
|
||||
runInAction(() => {
|
||||
this.recentStickyId = response.results[0]?.id;
|
||||
this.stickies[response.results[0]?.id] = response.results[0];
|
||||
});
|
||||
};
|
||||
fetchWorkspaceStickies = async (workspaceSlug: string, cursor?: string, per_page?: number) => {
|
||||
fetchNextWorkspaceStickies = async (workspaceSlug: string) => {
|
||||
try {
|
||||
const response = await this.stickyService.getStickies(workspaceSlug, cursor, per_page);
|
||||
if (!this.paginationInfo?.next_cursor || !this.paginationInfo.next_page_results || this.loader === "pagination") {
|
||||
return;
|
||||
}
|
||||
this.loader = "pagination";
|
||||
const response = await this.stickyService.getStickies(
|
||||
workspaceSlug,
|
||||
this.paginationInfo.next_cursor,
|
||||
this.searchQuery
|
||||
);
|
||||
|
||||
runInAction(() => {
|
||||
response.results.forEach((sticky) => {
|
||||
const { results, ...paginationInfo } = response;
|
||||
|
||||
// Add new stickies to store
|
||||
results.forEach((sticky) => {
|
||||
if (!this.workspaceStickies[workspaceSlug]?.includes(sticky.id)) {
|
||||
this.workspaceStickies[workspaceSlug] = [...(this.workspaceStickies[workspaceSlug] || []), sticky.id];
|
||||
}
|
||||
this.stickies[sticky.id] = sticky;
|
||||
});
|
||||
this.totalPages = response.total_pages;
|
||||
this.fetchingWorkspaceStickies = false;
|
||||
|
||||
// Update pagination info directly from backend
|
||||
set(this, "paginationInfo", paginationInfo);
|
||||
set(this, "loader", "loaded");
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.fetchingWorkspaceStickies = false;
|
||||
runInAction(() => {
|
||||
this.loader = "loaded";
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
fetchWorkspaceStickies = async (workspaceSlug: string) => {
|
||||
try {
|
||||
if (this.workspaceStickies[workspaceSlug]) {
|
||||
this.loader = "mutation";
|
||||
} else {
|
||||
this.loader = "init-loader";
|
||||
}
|
||||
|
||||
const response = await this.stickyService.getStickies(
|
||||
workspaceSlug,
|
||||
`${STICKIES_PER_PAGE}:0:0`,
|
||||
this.searchQuery
|
||||
);
|
||||
|
||||
runInAction(() => {
|
||||
const { results, ...paginationInfo } = response;
|
||||
|
||||
results.forEach((sticky) => {
|
||||
this.stickies[sticky.id] = sticky;
|
||||
});
|
||||
this.workspaceStickies[workspaceSlug] = results.map((sticky) => sticky.id);
|
||||
set(this, "paginationInfo", paginationInfo);
|
||||
this.loader = "loaded";
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
runInAction(() => {
|
||||
this.loader = "loaded";
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -138,16 +188,18 @@ export class StickyStore implements IStickyStore {
|
|||
const sticky = this.stickies[id];
|
||||
if (!sticky) return;
|
||||
try {
|
||||
this.stickies[id] = {
|
||||
...sticky,
|
||||
...updates,
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
runInAction(() => {
|
||||
Object.keys(updates).forEach((key) => {
|
||||
const currentStickyKey = key as keyof TSticky;
|
||||
set(this.stickies[id], key, updates[currentStickyKey] || undefined);
|
||||
});
|
||||
});
|
||||
this.recentStickyId = id;
|
||||
await this.stickyService.updateSticky(workspaceSlug, id, updates);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} catch (error) {
|
||||
console.error("Error in updating sticky:", error);
|
||||
this.stickies[id] = sticky;
|
||||
throw new Error();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -167,4 +219,53 @@ export class StickyStore implements IStickyStore {
|
|||
this.stickies[id] = sticky;
|
||||
}
|
||||
};
|
||||
|
||||
updateStickyPosition = async (
|
||||
workspaceSlug: string,
|
||||
stickyId: string,
|
||||
destinationId: string,
|
||||
edge: InstructionType
|
||||
) => {
|
||||
const previousSortOrder = this.stickies[stickyId].sort_order;
|
||||
try {
|
||||
let resultSequence = 10000;
|
||||
const workspaceStickies = this.workspaceStickies[workspaceSlug] || [];
|
||||
const stickies = workspaceStickies.map((id) => this.stickies[id]);
|
||||
const sortedStickies = orderBy(stickies, "sort_order", "desc").map((sticky) => sticky.id);
|
||||
const destinationSequence = this.stickies[destinationId]?.sort_order || undefined;
|
||||
|
||||
if (destinationSequence) {
|
||||
const destinationIndex = sortedStickies.findIndex((id) => id === destinationId);
|
||||
|
||||
if (edge === "reorder-above") {
|
||||
const prevSequence = this.stickies[sortedStickies[destinationIndex - 1]]?.sort_order || undefined;
|
||||
if (prevSequence) {
|
||||
resultSequence = (destinationSequence + prevSequence) / 2;
|
||||
} else {
|
||||
resultSequence = destinationSequence + resultSequence;
|
||||
}
|
||||
} else {
|
||||
// reorder-below
|
||||
resultSequence = destinationSequence - resultSequence;
|
||||
}
|
||||
}
|
||||
|
||||
runInAction(() => {
|
||||
this.stickies[stickyId] = {
|
||||
...this.stickies[stickyId],
|
||||
sort_order: resultSequence,
|
||||
};
|
||||
});
|
||||
|
||||
await this.stickyService.updateSticky(workspaceSlug, stickyId, {
|
||||
sort_order: resultSequence,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to move sticky");
|
||||
runInAction(() => {
|
||||
this.stickies[stickyId].sort_order = previousSortOrder;
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export interface IHomeStore {
|
|||
widgetsMap: Record<string, TWidgetEntityData>;
|
||||
widgets: THomeWidgetKeys[];
|
||||
// computed
|
||||
isAnyWidgetEnabled: boolean;
|
||||
orderedWidgets: THomeWidgetKeys[];
|
||||
//stores
|
||||
quickLinks: IWorkspaceLinkStore;
|
||||
|
|
@ -38,6 +39,7 @@ export class HomeStore implements IHomeStore {
|
|||
widgetsMap: observable,
|
||||
widgets: observable,
|
||||
// computed
|
||||
isAnyWidgetEnabled: computed,
|
||||
orderedWidgets: computed,
|
||||
// actions
|
||||
toggleWidgetSettings: action,
|
||||
|
|
@ -52,6 +54,10 @@ export class HomeStore implements IHomeStore {
|
|||
this.quickLinks = new WorkspaceLinkStore();
|
||||
}
|
||||
|
||||
get isAnyWidgetEnabled() {
|
||||
return Object.values(this.widgetsMap).some((widget) => widget.is_enabled);
|
||||
}
|
||||
|
||||
get orderedWidgets() {
|
||||
return orderBy(Object.values(this.widgetsMap), "sort_order", "desc").map((widget) => widget.key);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue