diff --git a/packages/types/src/view.d.ts b/packages/types/src/view.d.ts index 0cbc2d48d..94fd66581 100644 --- a/packages/types/src/view.d.ts +++ b/packages/types/src/view.d.ts @@ -53,23 +53,36 @@ export type TViewDisplayProperties = { updated_on: boolean; }; -export type TView = { - id: string; - workspace: string; - project: string | undefined; - name: string; - description: string; - query: string; +export type TViewFilterProps = { filters: TViewFilters; display_filters: TViewDisplayFilters; display_properties: TViewDisplayProperties; - access: TViewAccess; - owned_by: string; - sort_order: number; - is_locked: boolean; - is_pinned: boolean; - created_by: string; - updated_by: string; - created_at: Date; - updated_at: Date; +}; + +export type TViewFilterPartialProps = { + filters: Partial; + display_filters: Partial; + display_properties: Partial; +}; + +export type TView = { + readonly id: string; + readonly workspace: string; + readonly project: string | undefined; + name: string; + description: string; + readonly query: string; + filters: TViewFilters; + display_filters: TViewDisplayFilters; + display_properties: TViewDisplayProperties; + readonly access: TViewAccess; + readonly owned_by: string; + readonly sort_order: number; + readonly is_locked: boolean; + readonly is_pinned: boolean; + readonly is_favorite: boolean; + readonly created_by: string; + readonly updated_by: string; + readonly created_at: Date; + readonly updated_at: Date; }; diff --git a/web/services/view/project.service.ts b/web/services/view/project.service.ts index 63e282559..5c0f877b4 100644 --- a/web/services/view/project.service.ts +++ b/web/services/view/project.service.ts @@ -110,4 +110,30 @@ export class ProjectViewService extends APIService implements TViewService { throw error?.response; }); } + + async makeFavorite( + workspaceSlug: string, + viewId: string, + projectId: string | undefined = undefined + ): Promise { + if (!projectId) return undefined; + return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/favorite/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } + + async removeFavorite( + workspaceSlug: string, + viewId: string, + projectId: string | undefined = undefined + ): Promise { + if (!projectId) return undefined; + return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/unfavorite/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } } diff --git a/web/services/view/project_me.service.ts b/web/services/view/project_me.service.ts index c07c93848..cbda0c17b 100644 --- a/web/services/view/project_me.service.ts +++ b/web/services/view/project_me.service.ts @@ -110,4 +110,30 @@ export class ProjectViewMeService extends APIService implements TViewService { throw error?.response; }); } + + async makeFavorite( + workspaceSlug: string, + viewId: string, + projectId: string | undefined = undefined + ): Promise { + if (!projectId) return undefined; + return this.post(`/api/users/me/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/favorite/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } + + async removeFavorite( + workspaceSlug: string, + viewId: string, + projectId: string | undefined = undefined + ): Promise { + if (!projectId) return undefined; + return this.post(`/api/users/me/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/unfavorite/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } } diff --git a/web/services/view/types.d.ts b/web/services/view/types.d.ts index 322f1350d..f35ab1502 100644 --- a/web/services/view/types.d.ts +++ b/web/services/view/types.d.ts @@ -14,4 +14,6 @@ export type TViewService = { lock: (workspaceSlug: string, viewId: string, projectId?: string) => Promise; unlock: (workspaceSlug: string, viewId: string, projectId?: string) => Promise; duplicate: (workspaceSlug: string, viewId: string, projectId?: string) => Promise; + makeFavorite: (workspaceSlug: string, viewId: string, projectId?: string) => Promise; + removeFavorite: (workspaceSlug: string, viewId: string, projectId?: string) => Promise; }; diff --git a/web/services/view/workspace.service.ts b/web/services/view/workspace.service.ts index 9851a9dbf..dc2873273 100644 --- a/web/services/view/workspace.service.ts +++ b/web/services/view/workspace.service.ts @@ -73,4 +73,20 @@ export class WorkspaceViewService extends APIService implements TViewService { throw error?.response; }); } + + async makeFavorite(workspaceSlug: string, viewId: string): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/views/${viewId}/favorite/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } + + async removeFavorite(workspaceSlug: string, viewId: string): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/views/${viewId}/unfavorite/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } } diff --git a/web/services/view/workspace_me.service.ts b/web/services/view/workspace_me.service.ts index 987a640be..4e044e558 100644 --- a/web/services/view/workspace_me.service.ts +++ b/web/services/view/workspace_me.service.ts @@ -73,4 +73,20 @@ export class WorkspaceMeViewService extends APIService implements TViewService { throw error?.response; }); } + + async makeFavorite(workspaceSlug: string, viewId: string): Promise { + return this.post(`/api/users/me/workspaces/${workspaceSlug}/views/${viewId}/favorite/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } + + async removeFavorite(workspaceSlug: string, viewId: string): Promise { + return this.post(`/api/users/me/workspaces/${workspaceSlug}/views/${viewId}/unfavorite/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } } diff --git a/web/store/view/filters_helpers.ts b/web/store/view/filters_helpers.ts new file mode 100644 index 000000000..642f2329e --- /dev/null +++ b/web/store/view/filters_helpers.ts @@ -0,0 +1 @@ +export class ViewFilterHelpers {} diff --git a/web/store/view/view-root.store.ts b/web/store/view/view-root.store.ts index 02438c7cd..81057fce1 100644 --- a/web/store/view/view-root.store.ts +++ b/web/store/view/view-root.store.ts @@ -1,8 +1,8 @@ // types -import { action, computed, makeObservable, observable } from "mobx"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; // stores import { RootStore } from "store/root.store"; -import { Views } from "./view.store"; +import { ViewsStore } from "./view.store"; // types import { TViewService } from "services/view/types"; import { TView } from "@plane/types"; @@ -12,10 +12,9 @@ export type TLoader = "" | undefined; type TViewRoot = { // observables - viewMap: Record; + viewMap: Record; // computed viewIds: string[]; - // actions fetch: () => Promise; create: (view: Partial) => Promise; @@ -24,7 +23,7 @@ type TViewRoot = { }; export class ViewRoot implements TViewRoot { - viewMap: Record = {}; + viewMap: Record = {}; constructor(private store: RootStore, private service: TViewService) { makeObservable(this, { @@ -51,59 +50,51 @@ export class ViewRoot implements TViewRoot { // actions fetch = async () => { - try { - const { workspaceSlug, projectId } = this.store.app.router; - if (!workspaceSlug) return; + const { workspaceSlug, projectId } = this.store.app.router; + if (!workspaceSlug) return; - const views = await this.service.fetch(workspaceSlug, projectId); - if (!views) return; + const views = await this.service.fetch(workspaceSlug, projectId); + if (!views) return; + runInAction(() => { views.forEach((view) => { - set(this.viewMap, [view.id], new Views(this.store, view, this.service)); + set(this.viewMap, [view.id], new ViewsStore(this.store, view, this.service)); }); - } catch (error) { - console.log(error); - } + }); }; - create = async (_view: Partial) => { - try { - const { workspaceSlug, projectId } = this.store.app.router; - if (!workspaceSlug) return; + create = async (data: Partial) => { + const { workspaceSlug, projectId } = this.store.app.router; + if (!workspaceSlug) return; - const view = await this.service.create(workspaceSlug, _view, projectId); - if (!view) return; + const view = await this.service.create(workspaceSlug, data, projectId); + if (!view) return; - set(this.viewMap, [view.id], new Views(this.store, view, this.service)); - } catch (error) { - console.log(error); - } + runInAction(() => { + set(this.viewMap, [view.id], new ViewsStore(this.store, view, this.service)); + }); }; delete = async (viewId: string) => { - try { - const { workspaceSlug, projectId } = this.store.app.router; - if (!workspaceSlug) return; + const { workspaceSlug, projectId } = this.store.app.router; + if (!workspaceSlug) return; - await this.service.remove(workspaceSlug, viewId, projectId); + await this.service.remove(workspaceSlug, viewId, projectId); + runInAction(() => { delete this.viewMap[viewId]; - } catch (error) { - console.log(error); - } + }); }; duplicate = async (viewId: string) => { - try { - const { workspaceSlug, projectId } = this.store.app.router; - if (!workspaceSlug) return; + const { workspaceSlug, projectId } = this.store.app.router; + if (!workspaceSlug) return; - const view = await this.service.duplicate(workspaceSlug, viewId, projectId); - if (!view) return; + const view = await this.service.duplicate(workspaceSlug, viewId, projectId); + if (!view) return; - set(this.viewMap, [view.id], new Views(this.store, view, this.service)); - } catch (error) { - console.log(error); - } + runInAction(() => { + set(this.viewMap, [view.id], new ViewsStore(this.store, view, this.service)); + }); }; } diff --git a/web/store/view/view.store.ts b/web/store/view/view.store.ts index cd5b7ed06..c482f224b 100644 --- a/web/store/view/view.store.ts +++ b/web/store/view/view.store.ts @@ -1,25 +1,43 @@ -import { action, computed, makeObservable } from "mobx"; - +import { action, computed, makeObservable, observable, runInAction } from "mobx"; +import set from "lodash/set"; // store import { RootStore } from "store/root.store"; // types import { TViewService } from "services/view/types"; -import { TView, TViewFilters, TViewDisplayFilters, TViewDisplayProperties, TViewAccess } from "@plane/types"; +import { + TView, + TViewFilters, + TViewDisplayFilters, + TViewDisplayProperties, + TViewFilterProps, + TViewFilterPartialProps, + TViewAccess, +} from "@plane/types"; -export type TViews = TView & { +type TLoader = "submitting" | "submit" | undefined; + +export type TViewsStore = TView & { + // observables + loader: TLoader; + filtersToUpdate: TViewFilterPartialProps; // computed - user: undefined; + appliedFilters: TViewFilterProps | undefined; + appliedFiltersQueryParams: undefined; + // helper actions + updateFilters: (filters: Partial) => void; + updateDisplayFilters: (display_filters: Partial) => void; + updateDisplayProperties: (display_properties: Partial) => void; + resetFilterChanges: () => void; + saveFilterChanges: () => void; // actions - updateName: (name: string) => Promise; - updateDescription: (description: string) => Promise; - updateFilters: (filters: Partial) => Promise; - updateDisplayFilters: (display_filters: Partial) => Promise; - updateDisplayProperties: (display_properties: Partial) => Promise; lockView: () => Promise; unlockView: () => Promise; + makeFavorite: () => Promise; + removeFavorite: () => Promise; + update: (viewData: Partial) => Promise; }; -export class Views implements TViews { +export class ViewsStore implements TViewsStore { id: string; workspace: string; project: string | undefined; @@ -34,11 +52,19 @@ export class Views implements TViews { sort_order: number; is_locked: boolean; is_pinned: boolean; + is_favorite: boolean; created_by: string; updated_by: string; created_at: Date; updated_at: Date; + loader: TLoader = undefined; + filtersToUpdate: TViewFilterPartialProps = { + filters: {}, + display_filters: {}, + display_properties: {}, + }; + constructor(private store: RootStore, _view: TView, private service: TViewService) { this.id = _view.id; this.workspace = _view.workspace; @@ -54,133 +80,143 @@ export class Views implements TViews { this.sort_order = _view.sort_order; this.is_locked = _view.is_locked; this.is_pinned = _view.is_pinned; + this.is_favorite = _view.is_favorite; this.created_by = _view.created_by; this.updated_by = _view.updated_by; this.created_at = _view.created_at; this.updated_at = _view.updated_at; makeObservable(this, { + // observables + loader: observable, + filtersToUpdate: observable.ref, // computed - user: computed, - // actions - updateName: action, - updateDescription: action, + appliedFilters: computed, + appliedFiltersQueryParams: computed, + // helper actions updateFilters: action, updateDisplayFilters: action, updateDisplayProperties: action, + resetFilterChanges: action, + saveFilterChanges: action, + // actions + update: action, lockView: action, unlockView: action, }); } // computed - get user() { + get appliedFilters() { return undefined; } - // actions - updateName = async (name: string) => { - try { - const { workspaceSlug, projectId } = this.store.app.router; - if (!workspaceSlug) return; + get appliedFiltersQueryParams() { + return undefined; + } - const view = await this.service.update(workspaceSlug, this.id, { name: name }, projectId); - if (!view) return; - - this.name = view.name; - } catch (error) { - console.log(error); - } - }; - - updateDescription = async (description: string) => { - try { - const { workspaceSlug, projectId } = this.store.app.router; - if (!workspaceSlug) return; - - const view = await this.service.update(workspaceSlug, this.id, { description: description }, projectId); - if (!view) return; - - this.description = view.description; - } catch (error) { - console.log(error); - } - }; - - updateFilters = async (filters: Partial) => { - try { - const { workspaceSlug, projectId } = this.store.app.router; - if (!workspaceSlug) return; - - const viewFilters = this.filters; - const _filters = { ...viewFilters, ...filters }; - - const view = await this.service.update(workspaceSlug, this.id, { filters: _filters }, projectId); - if (!view) return; - - this.filters = view.filters; - } catch (error) { - console.log(error); - } + // helper actions + updateFilters = (filters: Partial) => { + runInAction(() => { + this.loader = "submit"; + this.filtersToUpdate.filters = filters; + }); }; updateDisplayFilters = async (display_filters: Partial) => { - try { - const { workspaceSlug, projectId } = this.store.app.router; - if (!workspaceSlug) return; - - const viewDisplayFilters = this.display_filters; - const _filters = { ...viewDisplayFilters, ...display_filters }; - - const view = await this.service.update(workspaceSlug, this.id, { display_filters: _filters }, projectId); - if (!view) return; - - this.display_filters = view.display_filters; - } catch (error) { - console.log(error); - } + runInAction(() => { + this.loader = "submit"; + this.filtersToUpdate.display_filters = display_filters; + }); }; updateDisplayProperties = async (display_properties: Partial) => { - try { - const { workspaceSlug, projectId } = this.store.app.router; - if (!workspaceSlug) return; - - const viewDisplayProperties = this.display_properties; - const _filters = { ...viewDisplayProperties, ...display_properties }; - - const view = await this.service.update(workspaceSlug, this.id, { display_properties: _filters }, projectId); - if (!view) return; - - this.display_properties = view.display_properties; - } catch (error) { - console.log(error); - } + runInAction(() => { + this.loader = "submit"; + this.filtersToUpdate.display_properties = display_properties; + }); }; + resetFilterChanges = () => { + runInAction(() => { + this.loader = undefined; + this.filtersToUpdate = { + filters: {}, + display_filters: {}, + display_properties: {}, + }; + }); + }; + + saveFilterChanges = async () => { + this.loader = "submitting"; + if (this.appliedFilters) await this.update(this.appliedFilters); + this.loader = undefined; + }; + + // actions lockView = async () => { - try { - const { workspaceSlug, projectId } = this.store.app.router; - if (!workspaceSlug) return; + const { workspaceSlug, projectId } = this.store.app.router; + if (!workspaceSlug) return; - const view = await this.service.lock(workspaceSlug, this.id, projectId); - if (!view) return; + const view = await this.service.lock(workspaceSlug, this.id, projectId); + if (!view) return; + runInAction(() => { this.is_locked = view.is_locked; - } catch (error) { - console.log(error); - } + }); }; unlockView = async () => { + const { workspaceSlug, projectId } = this.store.app.router; + if (!workspaceSlug) return; + + const view = await this.service.unlock(workspaceSlug, this.id, projectId); + if (!view) return; + + runInAction(() => { + this.is_locked = view.is_locked; + }); + }; + + makeFavorite = async () => { + const { workspaceSlug, projectId } = this.store.app.router; + if (!workspaceSlug) return; + + const view = await this.service.makeFavorite(workspaceSlug, this.id, projectId); + if (!view) return; + + runInAction(() => { + this.is_favorite = view.is_locked; + }); + }; + + removeFavorite = async () => { + const { workspaceSlug, projectId } = this.store.app.router; + if (!workspaceSlug) return; + + const view = await this.service.removeFavorite(workspaceSlug, this.id, projectId); + if (!view) return; + + runInAction(() => { + this.is_favorite = view.is_locked; + }); + }; + + update = async (viewData: Partial) => { try { const { workspaceSlug, projectId } = this.store.app.router; if (!workspaceSlug) return; - const view = await this.service.unlock(workspaceSlug, this.id, projectId); + const view = await this.service.update(workspaceSlug, this.id, viewData, projectId); if (!view) return; - this.is_locked = view.is_locked; + runInAction(() => { + Object.keys(viewData).forEach((key) => { + const _key = key as keyof TView; + set(this, _key, viewData[_key]); + }); + }); } catch (error) { console.log(error); }