From 275ea747724131848bd904324243901749f5cc87 Mon Sep 17 00:00:00 2001 From: Peng Xiao Date: Wed, 10 Jan 2024 05:25:37 +0000 Subject: [PATCH 01/92] chore(core): remove affine/cmdk package (#5552) patch cmdk based on https://github.com/pengx17/cmdk/tree/patch-1 fix https://github.com/toeverything/AFFiNE/issues/5548 --- .yarn/patches/cmdk-npm-0.2.0-302237a911.patch | 202 +++ package.json | 3 +- packages/common/cmdk/LICENSE.md | 21 - packages/common/cmdk/README.md | 3 - packages/common/cmdk/package.json | 15 - packages/common/cmdk/src/command-score.ts | 179 --- packages/common/cmdk/src/index.tsx | 1151 ----------------- packages/common/cmdk/tsconfig.json | 9 - packages/frontend/core/package.json | 2 +- .../core/src/components/pure/cmdk/data.tsx | 2 +- .../core/src/components/pure/cmdk/main.tsx | 14 +- .../src/components/pure/cmdk/not-found.tsx | 2 +- tsconfig.json | 3 - yarn.lock | 47 +- 14 files changed, 243 insertions(+), 1410 deletions(-) create mode 100644 .yarn/patches/cmdk-npm-0.2.0-302237a911.patch delete mode 100644 packages/common/cmdk/LICENSE.md delete mode 100644 packages/common/cmdk/README.md delete mode 100644 packages/common/cmdk/package.json delete mode 100644 packages/common/cmdk/src/command-score.ts delete mode 100644 packages/common/cmdk/src/index.tsx delete mode 100644 packages/common/cmdk/tsconfig.json diff --git a/.yarn/patches/cmdk-npm-0.2.0-302237a911.patch b/.yarn/patches/cmdk-npm-0.2.0-302237a911.patch new file mode 100644 index 0000000000..538dea1a17 --- /dev/null +++ b/.yarn/patches/cmdk-npm-0.2.0-302237a911.patch @@ -0,0 +1,202 @@ +diff --git a/dist/command-score.d.ts b/dist/command-score.d.ts +new file mode 100644 +index 0000000000000000000000000000000000000000..949ceeb1266241f7bf294b6df4422fd904e163c1 +--- /dev/null ++++ b/dist/command-score.d.ts +@@ -0,0 +1,3 @@ ++declare function commandScore(string: string, abbreviation: string): number; ++ ++export { commandScore }; +diff --git a/dist/command-score.js b/dist/command-score.js +new file mode 100644 +index 0000000000000000000000000000000000000000..0d88276d3d39315e68322c54aceb3cf9769ad732 +--- /dev/null ++++ b/dist/command-score.js +@@ -0,0 +1 @@ ++var p=Object.defineProperty;var a=Object.getOwnPropertyDescriptor;var H=Object.getOwnPropertyNames;var J=Object.prototype.hasOwnProperty;var k=(_,E)=>{for(var f in E)p(_,f,{get:E[f],enumerable:!0})},m=(_,E,f,C)=>{if(E&&typeof E=="object"||typeof E=="function")for(let c of H(E))!J.call(_,c)&&c!==f&&p(_,c,{get:()=>E[c],enumerable:!(C=a(E,c))||C.enumerable});return _};var B=_=>m(p({},"__esModule",{value:!0}),_);var Z={};k(Z,{commandScore:()=>V});module.exports=B(Z);var D=1,K=.9,W=.8,$=.17,u=.1,G=.999,y=.9999;var F=.99,j=/[\\\/_+.#"@\[\(\{&]/,q=/[\\\/_+.#"@\[\(\{&]/g,Q=/[\s-]/,Y=/[\s-]/g;function L(_,E,f,C,c,P,O){if(P===E.length)return c===_.length?D:F;var T=`${c},${P}`;if(O[T]!==void 0)return O[T];for(var U=C.charAt(P),A=f.indexOf(U,c),S=0,h,N,R,M;A>=0;)h=L(_,E,f,C,A+1,P+1,O),h>S&&(A===c?h*=D:j.test(_.charAt(A-1))?(h*=W,R=_.slice(c,A-1).match(q),R&&c>0&&(h*=Math.pow(G,R.length))):Q.test(_.charAt(A-1))?(h*=K,M=_.slice(c,A-1).match(Y),M&&c>0&&(h*=Math.pow(G,M.length))):(h*=$,c>0&&(h*=Math.pow(G,A-c))),_.charAt(A)!==E.charAt(P)&&(h*=y)),(hh&&(h=N*u)),h>S&&(S=h),A=f.indexOf(U,A+1);return O[T]=S,S}function X(_){return _.toLowerCase().replace(Y," ")}function V(_,E){return L(_,E,X(_),X(E),0,0,{})}0&&(module.exports={commandScore}); +diff --git a/dist/command-score.mjs b/dist/command-score.mjs +new file mode 100644 +index 0000000000000000000000000000000000000000..bb680ba9ce704741e08454e4884bcbb1697c7f2b +--- /dev/null ++++ b/dist/command-score.mjs +@@ -0,0 +1 @@ ++var U=1,Y=.9,a=.8,H=.17,p=.1,u=.999,J=.9999;var k=.99,m=/[\\\/_+.#"@\[\(\{&]/,B=/[\\\/_+.#"@\[\(\{&]/g,K=/[\s-]/,X=/[\s-]/g;function G(c,f,P,C,h,A,O){if(A===f.length)return h===c.length?U:k;var T=`${h},${A}`;if(O[T]!==void 0)return O[T];for(var L=C.charAt(A),E=P.indexOf(L,h),S=0,_,N,R,M;E>=0;)_=G(c,f,P,C,E+1,A+1,O),_>S&&(E===h?_*=U:m.test(c.charAt(E-1))?(_*=a,R=c.slice(h,E-1).match(B),R&&h>0&&(_*=Math.pow(u,R.length))):K.test(c.charAt(E-1))?(_*=Y,M=c.slice(h,E-1).match(X),M&&h>0&&(_*=Math.pow(u,M.length))):(_*=H,h>0&&(_*=Math.pow(u,E-h))),c.charAt(E)!==f.charAt(A)&&(_*=J)),(__&&(_=N*p)),_>S&&(S=_),E=P.indexOf(L,E+1);return O[T]=S,S}function D(c){return c.toLowerCase().replace(X," ")}function W(c,f){return G(c,f,D(c),D(f),0,0,{})}export{W as commandScore}; +diff --git a/dist/index.d.ts b/dist/index.d.ts +index faf9d6c5ad2de8af1abf49c9d70903bd1e911408..53eed433f6571e6a4ee34395a24d8cedefca3a33 100644 +--- a/dist/index.d.ts ++++ b/dist/index.d.ts +@@ -1,5 +1,6 @@ + import * as RadixDialog from '@radix-ui/react-dialog'; + import * as React from 'react'; ++export { commandScore } from './command-score.js'; + + declare type Children = { + children?: React.ReactNode; +@@ -30,6 +31,10 @@ declare const Command: React.ForwardRefExoticComponent number; ++ /** ++ * Optional default item value when it is initially rendered. ++ */ ++ defaultValue?: string; + /** + * Optional controlled state of the selected command menu item. + */ +@@ -42,6 +47,10 @@ declare const Command: React.ForwardRefExoticComponent>; + /** + * Command menu item. Becomes active on pointer enter or through keyboard navigation. +@@ -58,7 +67,13 @@ declare const Item: React.ForwardRefExoticComponent>; ++declare type Group = { ++ id: string; ++ forceMount?: boolean; ++}; + /** + * Group command menu items together with a heading. + * Grouped items are always shown together. +@@ -68,6 +83,8 @@ declare const Group: React.ForwardRefExoticComponent>; + /** + * A visual and semantic separator between items or groups. +@@ -115,6 +132,10 @@ declare const Dialog: React.ForwardRefExoticComponent number; ++ /** ++ * Optional default item value when it is initially rendered. ++ */ ++ defaultValue?: string; + /** + * Optional controlled state of the selected command menu item. + */ +@@ -127,7 +148,15 @@ declare const Dialog: React.ForwardRefExoticComponent>; +@@ -138,7 +167,7 @@ declare const Empty: React.ForwardRefExoticComponent>; +@@ -158,6 +187,10 @@ declare const pkg: React.ForwardRefExoticComponent number; ++ /** ++ * Optional default item value when it is initially rendered. ++ */ ++ defaultValue?: string; + /** + * Optional controlled state of the selected command menu item. + */ +@@ -170,6 +203,10 @@ declare const pkg: React.ForwardRefExoticComponent> & { + List: React.ForwardRefExoticComponent>; + Item: React.ForwardRefExoticComponent & { +@@ -182,6 +219,8 @@ declare const pkg: React.ForwardRefExoticComponent>; + Input: React.ForwardRefExoticComponent, "value" | "onChange" | "type"> & { + /** +@@ -198,6 +237,8 @@ declare const pkg: React.ForwardRefExoticComponent>; + Separator: React.ForwardRefExoticComponent number; ++ /** ++ * Optional default item value when it is initially rendered. ++ */ ++ defaultValue?: string; + /** + * Optional controlled state of the selected command menu item. + */ +@@ -231,12 +276,20 @@ declare const pkg: React.ForwardRefExoticComponent>; + Empty: React.ForwardRefExoticComponent>; +- Loading: React.ForwardRefExoticComponent>; +diff --git a/dist/index.js b/dist/index.js +index 64801d481bdc4872c6ace390225105ab21508c0c..038dbe22b7c0eecb6babe424d1425131fac4d242 100644 +--- a/dist/index.js ++++ b/dist/index.js +@@ -1 +1 @@ +-var Se=Object.create;var F=Object.defineProperty;var Ce=Object.getOwnPropertyDescriptor;var ye=Object.getOwnPropertyNames;var xe=Object.getPrototypeOf,Te=Object.prototype.hasOwnProperty;var Le=(r,o)=>{for(var n in o)F(r,n,{get:o[n],enumerable:!0})},re=(r,o,n,a)=>{if(o&&typeof o=="object"||typeof o=="function")for(let l of ye(o))!Te.call(r,l)&&l!==n&&F(r,l,{get:()=>o[l],enumerable:!(a=Ce(o,l))||a.enumerable});return r};var z=(r,o,n)=>(n=r!=null?Se(xe(r)):{},re(o||!r||!r.__esModule?F(n,"default",{value:r,enumerable:!0}):n,r)),we=r=>re(F({},"__esModule",{value:!0}),r);var _e={};Le(_e,{Command:()=>Me,CommandDialog:()=>pe,CommandEmpty:()=>ge,CommandGroup:()=>ue,CommandInput:()=>fe,CommandItem:()=>le,CommandList:()=>me,CommandLoading:()=>ve,CommandRoot:()=>J,CommandSeparator:()=>de,useCommandState:()=>y});module.exports=we(_e);var C=z(require("@radix-ui/react-dialog")),t=z(require("react")),oe=z(require("command-score")),De='[cmdk-list-sizer=""]',M='[cmdk-group=""]',U='[cmdk-group-items=""]',Ie='[cmdk-group-heading=""]',ae='[cmdk-item=""]',ne=`${ae}:not([aria-disabled="true"])`,B="cmdk-item-select",S="data-value",Pe=(r,o)=>(0,oe.default)(r,o),se=t.createContext(void 0),k=()=>t.useContext(se),ie=t.createContext(void 0),W=()=>t.useContext(ie),ce=t.createContext(void 0),J=t.forwardRef((r,o)=>{let n=t.useRef(null),a=T(()=>({search:"",value:"",filtered:{count:0,items:new Map,groups:new Set}})),l=T(()=>new Set),u=T(()=>new Map),p=T(()=>new Map),f=T(()=>new Set),d=Re(r),{label:v,children:E,value:R,onValueChange:w,filter:O,shouldFilter:he,...D}=r,K=t.useId(),g=t.useId(),A=t.useId(),x=Oe();L(()=>{if(R!==void 0){let e=R.trim().toLowerCase();a.current.value=e,x(6,X),h.emit()}},[R]);let h=t.useMemo(()=>({subscribe:e=>(f.current.add(e),()=>f.current.delete(e)),snapshot:()=>a.current,setState:(e,c,i)=>{var s,m,b;if(!Object.is(a.current[e],c)){if(a.current[e]=c,e==="search")q(),V(),x(1,j);else if(e==="value")if(((s=d.current)==null?void 0:s.value)!==void 0){(b=(m=d.current).onValueChange)==null||b.call(m,c);return}else i||x(5,X);h.emit()}},emit:()=>{f.current.forEach(e=>e())}}),[]),G=t.useMemo(()=>({value:(e,c)=>{c!==p.current.get(e)&&(p.current.set(e,c),a.current.filtered.items.set(e,Q(c)),x(2,()=>{V(),h.emit()}))},item:(e,c)=>(l.current.add(e),c&&(u.current.has(c)?u.current.get(c).add(e):u.current.set(c,new Set([e]))),x(3,()=>{q(),V(),a.current.value||j(),h.emit()}),()=>{p.current.delete(e),l.current.delete(e),a.current.filtered.items.delete(e),x(4,()=>{q(),j(),h.emit()})}),group:e=>(u.current.has(e)||u.current.set(e,new Set),()=>{p.current.delete(e),u.current.delete(e)}),filter:()=>d.current.shouldFilter,label:v||r["aria-label"],listId:K,inputId:A,labelId:g}),[]);function Q(e){var i;let c=((i=d.current)==null?void 0:i.filter)??Pe;return e?c(e,a.current.search):0}function V(){if(!n.current||!a.current.search||d.current.shouldFilter===!1)return;let e=a.current.filtered.items,c=[];a.current.filtered.groups.forEach(s=>{let m=u.current.get(s),b=0;m.forEach(P=>{let Ee=e.get(P);b=Math.max(Ee,b)}),c.push([s,b])});let i=n.current.querySelector(De);I().sort((s,m)=>{let b=s.getAttribute(S),P=m.getAttribute(S);return(e.get(P)??0)-(e.get(b)??0)}).forEach(s=>{let m=s.closest(U);m?m.appendChild(s.parentElement===m?s:s.closest(`${U} > *`)):i.appendChild(s.parentElement===i?s:s.closest(`${U} > *`))}),c.sort((s,m)=>m[1]-s[1]).forEach(s=>{let m=n.current.querySelector(`${M}[${S}="${s[0]}"]`);m==null||m.parentElement.appendChild(m)})}function j(){let e=I().find(i=>!i.ariaDisabled),c=e==null?void 0:e.getAttribute(S);h.setState("value",c||void 0)}function q(){if(!a.current.search||d.current.shouldFilter===!1){a.current.filtered.count=l.current.size;return}a.current.filtered.groups=new Set;let e=0;for(let c of l.current){let i=p.current.get(c),s=Q(i);a.current.filtered.items.set(c,s),s>0&&e++}for(let[c,i]of u.current)for(let s of i)if(a.current.filtered.items.get(s)>0){a.current.filtered.groups.add(c);break}a.current.filtered.count=e}function X(){var c,i,s;let e=_();e&&(((c=e.parentElement)==null?void 0:c.firstChild)===e&&((s=(i=e.closest(M))==null?void 0:i.querySelector(Ie))==null||s.scrollIntoView({block:"nearest"})),e.scrollIntoView({block:"nearest"}))}function _(){return n.current.querySelector(`${ae}[aria-selected="true"]`)}function I(){return Array.from(n.current.querySelectorAll(ne))}function $(e){let i=I()[e];i&&h.setState("value",i.getAttribute(S))}function N(e){var b;let c=_(),i=I(),s=i.findIndex(P=>P===c),m=i[s+e];(b=d.current)!=null&&b.loop&&(m=s+e<0?i[i.length-1]:s+e===i.length?i[0]:i[s+e]),m&&h.setState("value",m.getAttribute(S))}function Y(e){let c=_(),i=c==null?void 0:c.closest(M),s;for(;i&&!s;)i=e>0?ke(i,M):He(i,M),s=i==null?void 0:i.querySelector(ne);s?h.setState("value",s.getAttribute(S)):N(e)}let Z=()=>$(I().length-1),ee=e=>{e.preventDefault(),e.metaKey?Z():e.altKey?Y(1):N(1)},te=e=>{e.preventDefault(),e.metaKey?$(0):e.altKey?Y(-1):N(-1)};return t.createElement("div",{ref:H([n,o]),...D,"cmdk-root":"",onKeyDown:e=>{var c;if((c=D.onKeyDown)==null||c.call(D,e),!e.defaultPrevented)switch(e.key){case"n":case"j":{e.ctrlKey&&ee(e);break}case"ArrowDown":{ee(e);break}case"p":case"k":{e.ctrlKey&&te(e);break}case"ArrowUp":{te(e);break}case"Home":{e.preventDefault(),$(0);break}case"End":{e.preventDefault(),Z();break}case"Enter":{e.preventDefault();let i=_();if(i){let s=new Event(B);i.dispatchEvent(s)}}}}},t.createElement("label",{"cmdk-label":"",htmlFor:G.inputId,id:G.labelId,style:Ae},v),t.createElement(ie.Provider,{value:h},t.createElement(se.Provider,{value:G},E)))}),le=t.forwardRef((r,o)=>{let n=t.useId(),a=t.useRef(null),l=t.useContext(ce),u=k(),p=Re(r);L(()=>u.item(n,l),[]);let f=be(n,a,[r.value,r.children,a]),d=W(),v=y(g=>g.value&&g.value===f.current),E=y(g=>u.filter()===!1?!0:g.search?g.filtered.items.get(n)>0:!0);t.useEffect(()=>{let g=a.current;if(!(!g||r.disabled))return g.addEventListener(B,R),()=>g.removeEventListener(B,R)},[E,r.onSelect,r.disabled]);function R(){var g,A;(A=(g=p.current).onSelect)==null||A.call(g,f.current)}function w(){d.setState("value",f.current,!0)}if(!E)return null;let{disabled:O,value:he,onSelect:D,...K}=r;return t.createElement("div",{ref:H([a,o]),...K,"cmdk-item":"",role:"option","aria-disabled":O||void 0,"aria-selected":v||void 0,"data-selected":v||void 0,onPointerMove:O?void 0:w,onClick:O?void 0:R},r.children)}),ue=t.forwardRef((r,o)=>{let{heading:n,children:a,...l}=r,u=t.useId(),p=t.useRef(null),f=t.useRef(null),d=t.useId(),v=k(),E=y(w=>v.filter()===!1?!0:w.search?w.filtered.groups.has(u):!0);L(()=>v.group(u),[]),be(u,p,[r.value,r.heading,f]);let R=t.createElement(ce.Provider,{value:u},a);return t.createElement("div",{ref:H([p,o]),...l,"cmdk-group":"",role:"presentation",hidden:E?void 0:!0},n&&t.createElement("div",{ref:f,"cmdk-group-heading":"","aria-hidden":!0,id:d},n),t.createElement("div",{"cmdk-group-items":"",role:"group","aria-labelledby":n?d:void 0},R))}),de=t.forwardRef((r,o)=>{let{alwaysRender:n,...a}=r,l=t.useRef(null),u=y(p=>!p.search);return!n&&!u?null:t.createElement("div",{ref:H([l,o]),...a,"cmdk-separator":"",role:"separator"})}),fe=t.forwardRef((r,o)=>{let{onValueChange:n,...a}=r,l=r.value!=null,u=W(),p=y(d=>d.search),f=k();return t.useEffect(()=>{r.value!=null&&u.setState("search",r.value)},[r.value]),t.createElement("input",{ref:o,...a,"cmdk-input":"",autoComplete:"off",autoCorrect:"off",spellCheck:!1,"aria-autocomplete":"list",role:"combobox","aria-expanded":!0,"aria-controls":f.listId,"aria-labelledby":f.labelId,id:f.inputId,type:"text",value:l?r.value:p,onChange:d=>{l||u.setState("search",d.target.value),n==null||n(d.target.value)}})}),me=t.forwardRef((r,o)=>{let{children:n,...a}=r,l=t.useRef(null),u=t.useRef(null),p=k();return t.useEffect(()=>{if(u.current&&l.current){let f=u.current,d=l.current,v,E=new ResizeObserver(()=>{v=requestAnimationFrame(()=>{let R=f.getBoundingClientRect().height;d.style.setProperty("--cmdk-list-height",R.toFixed(1)+"px")})});return E.observe(f),()=>{cancelAnimationFrame(v),E.unobserve(f)}}},[]),t.createElement("div",{ref:H([l,o]),...a,"cmdk-list":"",role:"listbox","aria-label":"Suggestions",id:p.listId,"aria-labelledby":p.inputId},t.createElement("div",{ref:u,"cmdk-list-sizer":""},n))}),pe=t.forwardRef((r,o)=>{let{open:n,onOpenChange:a,container:l,...u}=r;return t.createElement(C.Root,{open:n,onOpenChange:a},t.createElement(C.Portal,{container:l},t.createElement(C.Overlay,{"cmdk-overlay":""}),t.createElement(C.Content,{"aria-label":r.label,"cmdk-dialog":""},t.createElement(J,{ref:o,...u}))))}),ge=t.forwardRef((r,o)=>{let n=t.useRef(!0),a=y(l=>l.filtered.count===0);return t.useEffect(()=>{n.current=!1},[]),n.current||!a?null:t.createElement("div",{ref:o,...r,"cmdk-empty":"",role:"presentation"})}),ve=t.forwardRef((r,o)=>{let{progress:n,children:a,...l}=r;return t.createElement("div",{ref:o,...l,"cmdk-loading":"",role:"progressbar","aria-valuenow":n,"aria-valuemin":0,"aria-valuemax":100,"aria-label":"Loading..."},t.createElement("div",{"aria-hidden":!0},a))}),Me=Object.assign(J,{List:me,Item:le,Input:fe,Group:ue,Separator:de,Dialog:pe,Empty:ge,Loading:ve});function ke(r,o){let n=r.nextElementSibling;for(;n;){if(n.matches(o))return n;n=n.nextElementSibling}}function He(r,o){let n=r.previousElementSibling;for(;n;){if(n.matches(o))return n;n=n.previousElementSibling}}function Re(r){let o=t.useRef(r);return L(()=>{o.current=r}),o}var L=typeof window>"u"?t.useEffect:t.useLayoutEffect;function T(r){let o=t.useRef();return o.current===void 0&&(o.current=r()),o}function H(r){return o=>{r.forEach(n=>{typeof n=="function"?n(o):n!=null&&(n.current=o)})}}function y(r){let o=W(),n=()=>r(o.snapshot());return t.useSyncExternalStore(o.subscribe,n,n)}function be(r,o,n){let a=t.useRef(),l=k();return L(()=>{var p;let u=(()=>{var f;for(let d of n){if(typeof d=="string")return d.trim().toLowerCase();if(typeof d=="object"&&"current"in d&&d.current)return(f=d.current.textContent)==null?void 0:f.trim().toLowerCase()}})();l.value(r,u),(p=o.current)==null||p.setAttribute(S,u),a.current=u}),a}var Oe=()=>{let[r,o]=t.useState(),n=T(()=>new Map);return L(()=>{n.current.forEach(a=>a()),n.current=new Map},[r]),(a,l)=>{n.current.set(a,l),o({})}},Ae={position:"absolute",width:"1px",height:"1px",padding:"0",margin:"-1px",overflow:"hidden",clip:"rect(0, 0, 0, 0)",whiteSpace:"nowrap",borderWidth:"0"};0&&(module.exports={Command,CommandDialog,CommandEmpty,CommandGroup,CommandInput,CommandItem,CommandList,CommandLoading,CommandRoot,CommandSeparator,useCommandState}); ++var De=Object.create;var G=Object.defineProperty;var xe=Object.getOwnPropertyDescriptor;var Ae=Object.getOwnPropertyNames;var we=Object.getPrototypeOf,_e=Object.prototype.hasOwnProperty;var Oe=(t,o)=>{for(var n in o)G(t,n,{get:o[n],enumerable:!0})},ie=(t,o,n,a)=>{if(o&&typeof o=="object"||typeof o=="function")for(let c of Ae(o))!_e.call(t,c)&&c!==n&&G(t,c,{get:()=>o[c],enumerable:!(a=xe(o,c))||a.enumerable});return t};var le=(t,o,n)=>(n=t!=null?De(we(t)):{},ie(o||!t||!t.__esModule?G(n,"default",{value:t,enumerable:!0}):n,t)),ke=t=>ie(G({},"__esModule",{value:!0}),t);var ze={};Oe(ze,{Command:()=>Be,CommandDialog:()=>be,CommandEmpty:()=>Te,CommandGroup:()=>he,CommandInput:()=>Ce,CommandItem:()=>ge,CommandList:()=>Se,CommandLoading:()=>ye,CommandRoot:()=>te,CommandSeparator:()=>Ee,commandScore:()=>W,useCommandState:()=>P});module.exports=ke(ze);var M=le(require("@radix-ui/react-dialog")),r=le(require("react"));var ue=1,Ie=.9,He=.8,Ne=.17,X=.1,Y=.999,Ge=.9999;var Ke=.99,Ve=/[\\\/_+.#"@\[\(\{&]/,Fe=/[\\\/_+.#"@\[\(\{&]/g,$e=/[\s-]/,fe=/[\s-]/g;function J(t,o,n,a,c,i,f){if(i===o.length)return c===t.length?ue:Ke;var v=`${c},${i}`;if(f[v]!==void 0)return f[v];for(var d=a.charAt(i),p=n.indexOf(d,c),R=0,m,S,T,E;p>=0;)m=J(t,o,n,a,p+1,i+1,f),m>R&&(p===c?m*=ue:Ve.test(t.charAt(p-1))?(m*=He,T=t.slice(c,p-1).match(Fe),T&&c>0&&(m*=Math.pow(Y,T.length))):$e.test(t.charAt(p-1))?(m*=Ie,E=t.slice(c,p-1).match(fe),E&&c>0&&(m*=Math.pow(Y,E.length))):(m*=Ne,c>0&&(m*=Math.pow(Y,p-c))),t.charAt(p)!==o.charAt(i)&&(m*=Ge)),(mm&&(m=S*X)),m>R&&(R=m),p=n.indexOf(d,p+1);return f[v]=R,R}function de(t){return t.toLowerCase().replace(fe," ")}function W(t,o){return J(t,o,de(t),de(o),0,0,{})}var je='[cmdk-list-sizer=""]',k='[cmdk-group=""]',z='[cmdk-group-items=""]',Ue='[cmdk-group-heading=""]',Z='[cmdk-item=""]',me=`${Z}:not([aria-disabled="true"])`,Q="cmdk-item-select",D="data-value",qe=(t,o)=>W(t,o),pe=r.createContext(void 0),I=()=>r.useContext(pe),Re=r.createContext(void 0),ee=()=>r.useContext(Re),ve=r.createContext(void 0),te=r.forwardRef((t,o)=>{let n=r.useRef(null),a=x(()=>{var e;return{search:"",value:t.value??((e=t.defaultValue)==null?void 0:e.toLowerCase())??"",filtered:{count:0,items:new Map,groups:new Set}}}),c=x(()=>new Set),i=x(()=>new Map),f=x(()=>new Map),v=x(()=>new Set),d=Pe(t),{label:p,children:R,value:m,onValueChange:S,filter:T,shouldFilter:E,vimBindings:K=!0,...w}=t,V=r.useId(),N=r.useId(),h=r.useId(),y=Je();A(()=>{if(m!==void 0){let e=m.trim().toLowerCase();a.current.value=e,y(6,ne),b.emit()}},[m]);let b=r.useMemo(()=>({subscribe:e=>(v.current.add(e),()=>v.current.delete(e)),snapshot:()=>a.current,setState:(e,u,s)=>{var l,g,C;if(!Object.is(a.current[e],u)){if(a.current[e]=u,e==="search")U(),$(),y(1,j);else if(e==="value")if(((l=d.current)==null?void 0:l.value)!==void 0){let L=u??"";(C=(g=d.current).onValueChange)==null||C.call(g,L);return}else s||y(5,ne);b.emit()}},emit:()=>{v.current.forEach(e=>e())}}),[]),F=r.useMemo(()=>({value:(e,u)=>{u!==f.current.get(e)&&(f.current.set(e,u),a.current.filtered.items.set(e,re(u)),y(2,()=>{$(),b.emit()}))},item:(e,u)=>(c.current.add(e),u&&(i.current.has(u)?i.current.get(u).add(e):i.current.set(u,new Set([e]))),y(3,()=>{U(),$(),a.current.value||j(),b.emit()}),()=>{f.current.delete(e),c.current.delete(e),a.current.filtered.items.delete(e);let s=_();y(4,()=>{U(),(s==null?void 0:s.getAttribute("id"))===e&&j(),b.emit()})}),group:e=>(i.current.has(e)||i.current.set(e,new Set),()=>{f.current.delete(e),i.current.delete(e)}),filter:()=>d.current.shouldFilter,label:p||t["aria-label"],commandRef:n,listId:V,inputId:h,labelId:N}),[]);function re(e){var s;let u=((s=d.current)==null?void 0:s.filter)??qe;return e?u(e,a.current.search):0}function $(){if(!n.current||!a.current.search||d.current.shouldFilter===!1)return;let e=a.current.filtered.items,u=[];a.current.filtered.groups.forEach(l=>{let g=i.current.get(l),C=0;g.forEach(L=>{let Le=e.get(L);C=Math.max(Le,C)}),u.push([l,C])});let s=n.current.querySelector(je);O().sort((l,g)=>{let C=l.getAttribute("id"),L=g.getAttribute("id");return(e.get(L)??0)-(e.get(C)??0)}).forEach(l=>{let g=l.closest(z);g?g.appendChild(l.parentElement===g?l:l.closest(`${z} > *`)):s.appendChild(l.parentElement===s?l:l.closest(`${z} > *`))}),u.sort((l,g)=>g[1]-l[1]).forEach(l=>{let g=n.current.querySelector(`${k}[${D}="${l[0]}"]`);g==null||g.parentElement.appendChild(g)})}function j(){let e=O().find(s=>!s.ariaDisabled),u=e==null?void 0:e.getAttribute(D);b.setState("value",u||void 0)}function U(){if(!a.current.search||d.current.shouldFilter===!1){a.current.filtered.count=c.current.size;return}a.current.filtered.groups=new Set;let e=0;for(let u of c.current){let s=f.current.get(u),l=re(s);a.current.filtered.items.set(u,l),l>0&&e++}for(let[u,s]of i.current)for(let l of s)if(a.current.filtered.items.get(l)>0){a.current.filtered.groups.add(u);break}a.current.filtered.count=e}function ne(){var u,s,l;let e=_();e&&(((u=e.parentElement)==null?void 0:u.firstChild)===e&&((l=(s=e.closest(k))==null?void 0:s.querySelector(Ue))==null||l.scrollIntoView({block:"nearest"})),e.scrollIntoView({block:"nearest"}))}function _(){var e;return(e=n.current)==null?void 0:e.querySelector(`${Z}[aria-selected="true"]`)}function O(){return Array.from(n.current.querySelectorAll(me))}function q(e){let s=O()[e];s&&b.setState("value",s.getAttribute(D))}function B(e){var C;let u=_(),s=O(),l=s.findIndex(L=>L===u),g=s[l+e];(C=d.current)!=null&&C.loop&&(g=l+e<0?s[s.length-1]:l+e===s.length?s[0]:s[l+e]),g&&b.setState("value",g.getAttribute(D))}function oe(e){let u=_(),s=u==null?void 0:u.closest(k),l;for(;s&&!l;)s=e>0?Xe(s,k):Ye(s,k),l=s==null?void 0:s.querySelector(me);l?b.setState("value",l.getAttribute(D)):B(e)}let ae=()=>q(O().length-1),ce=e=>{e.preventDefault(),e.metaKey?ae():e.altKey?oe(1):B(1)},se=e=>{e.preventDefault(),e.metaKey?q(0):e.altKey?oe(-1):B(-1)};return r.createElement("div",{ref:H([n,o]),...w,"cmdk-root":"",onKeyDown:e=>{var u;if((u=w.onKeyDown)==null||u.call(w,e),!e.defaultPrevented)switch(e.key){case"n":case"j":{K&&e.ctrlKey&&ce(e);break}case"ArrowDown":{ce(e);break}case"p":case"k":{K&&e.ctrlKey&&se(e);break}case"ArrowUp":{se(e);break}case"Home":{e.preventDefault(),q(0);break}case"End":{e.preventDefault(),ae();break}case"Enter":if(!e.nativeEvent.isComposing){e.preventDefault();let s=_();if(s){let l=new Event(Q);s.dispatchEvent(l)}}}}},r.createElement("label",{"cmdk-label":"",htmlFor:F.inputId,id:F.labelId,style:We},p),r.createElement(Re.Provider,{value:b},r.createElement(pe.Provider,{value:F},R)))}),ge=r.forwardRef((t,o)=>{var N;let n=r.useId(),a=r.useRef(null),c=r.useContext(ve),i=I(),f=Pe(t),v=((N=f.current)==null?void 0:N.forceMount)??(c==null?void 0:c.forceMount);A(()=>i.item(n,c==null?void 0:c.id),[]);let d=Me(n,a,[t.value,t.children,a]),p=ee(),R=P(h=>h.value&&h.value===d.current),m=P(h=>v||i.filter()===!1?!0:h.search?h.filtered.items.get(n)>0:!0);r.useEffect(()=>{let h=a.current;if(!(!h||t.disabled))return h.addEventListener(Q,S),()=>h.removeEventListener(Q,S)},[m,t.onSelect,t.disabled]);function S(){var h,y;T(),(y=(h=f.current).onSelect)==null||y.call(h,d.current)}function T(){p.setState("value",d.current,!0)}if(!m)return null;let{disabled:E,value:K,onSelect:w,...V}=t;return r.createElement("div",{ref:H([a,o]),...V,id:n,"cmdk-item":"",role:"option","aria-disabled":E||void 0,"aria-selected":R||void 0,"data-disabled":E||void 0,"data-selected":R||void 0,onPointerMove:E?void 0:T,onClick:E?void 0:S},t.children)}),he=r.forwardRef((t,o)=>{let{heading:n,children:a,forceMount:c,...i}=t,f=r.useId(),v=r.useRef(null),d=r.useRef(null),p=r.useId(),R=I(),m=P(E=>c||R.filter()===!1?!0:E.search?E.filtered.groups.has(f):!0);A(()=>R.group(f),[]),Me(f,v,[t.value,t.heading,d]);let S=r.useMemo(()=>({id:f,forceMount:c}),[c]),T=r.createElement(ve.Provider,{value:S},a);return r.createElement("div",{ref:H([v,o]),...i,"cmdk-group":"",role:"presentation",hidden:m?void 0:!0},n&&r.createElement("div",{ref:d,"cmdk-group-heading":"","aria-hidden":!0,id:p},n),r.createElement("div",{"cmdk-group-items":"",role:"group","aria-labelledby":n?p:void 0},T))}),Ee=r.forwardRef((t,o)=>{let{alwaysRender:n,...a}=t,c=r.useRef(null),i=P(f=>!f.search);return!n&&!i?null:r.createElement("div",{ref:H([c,o]),...a,"cmdk-separator":"",role:"separator"})}),Ce=r.forwardRef((t,o)=>{let{onValueChange:n,...a}=t,c=t.value!=null,i=ee(),f=P(R=>R.search),v=P(R=>R.value),d=I(),p=r.useMemo(()=>{var m;let R=(m=d.commandRef.current)==null?void 0:m.querySelector(`${Z}[${D}="${v}"]`);return R==null?void 0:R.getAttribute("id")},[v,d.commandRef]);return r.useEffect(()=>{t.value!=null&&i.setState("search",t.value)},[t.value]),r.createElement("input",{ref:o,...a,"cmdk-input":"",autoComplete:"off",autoCorrect:"off",spellCheck:!1,"aria-autocomplete":"list",role:"combobox","aria-expanded":!0,"aria-controls":d.listId,"aria-labelledby":d.labelId,"aria-activedescendant":p,id:d.inputId,type:"text",value:c?t.value:f,onChange:R=>{c||i.setState("search",R.target.value),n==null||n(R.target.value)}})}),Se=r.forwardRef((t,o)=>{let{children:n,...a}=t,c=r.useRef(null),i=r.useRef(null),f=I();return r.useEffect(()=>{if(i.current&&c.current){let v=i.current,d=c.current,p,R=new ResizeObserver(()=>{p=requestAnimationFrame(()=>{let m=v.offsetHeight;d.style.setProperty("--cmdk-list-height",m.toFixed(1)+"px")})});return R.observe(v),()=>{cancelAnimationFrame(p),R.unobserve(v)}}},[]),r.createElement("div",{ref:H([c,o]),...a,"cmdk-list":"",role:"listbox","aria-label":"Suggestions",id:f.listId,"aria-labelledby":f.inputId},r.createElement("div",{ref:i,"cmdk-list-sizer":""},n))}),be=r.forwardRef((t,o)=>{let{open:n,onOpenChange:a,overlayClassName:c,contentClassName:i,container:f,...v}=t;return r.createElement(M.Root,{open:n,onOpenChange:a},r.createElement(M.Portal,{container:f},r.createElement(M.Overlay,{"cmdk-overlay":"",className:c}),r.createElement(M.Content,{"aria-label":t.label,"cmdk-dialog":"",className:i},r.createElement(te,{ref:o,...v}))))}),Te=r.forwardRef((t,o)=>{let n=r.useRef(!0),a=P(c=>c.filtered.count===0);return r.useEffect(()=>{n.current=!1},[]),n.current||!a?null:r.createElement("div",{ref:o,...t,"cmdk-empty":"",role:"presentation"})}),ye=r.forwardRef((t,o)=>{let{progress:n,children:a,...c}=t;return r.createElement("div",{ref:o,...c,"cmdk-loading":"",role:"progressbar","aria-valuenow":n,"aria-valuemin":0,"aria-valuemax":100,"aria-label":"Loading..."},r.createElement("div",{"aria-hidden":!0},a))}),Be=Object.assign(te,{List:Se,Item:ge,Input:Ce,Group:he,Separator:Ee,Dialog:be,Empty:Te,Loading:ye});function Xe(t,o){let n=t.nextElementSibling;for(;n;){if(n.matches(o))return n;n=n.nextElementSibling}}function Ye(t,o){let n=t.previousElementSibling;for(;n;){if(n.matches(o))return n;n=n.previousElementSibling}}function Pe(t){let o=r.useRef(t);return A(()=>{o.current=t}),o}var A=typeof window>"u"?r.useEffect:r.useLayoutEffect;function x(t){let o=r.useRef();return o.current===void 0&&(o.current=t()),o}function H(t){return o=>{t.forEach(n=>{typeof n=="function"?n(o):n!=null&&(n.current=o)})}}function P(t){let o=ee(),n=()=>t(o.snapshot());return r.useSyncExternalStore(o.subscribe,n,n)}function Me(t,o,n){let a=r.useRef(),c=I();return A(()=>{var f;let i=(()=>{var v;for(let d of n){if(typeof d=="string")return d.trim().toLowerCase();if(typeof d=="object"&&"current"in d)return d.current?(v=d.current.textContent)==null?void 0:v.trim().toLowerCase():a.current}})();c.value(t,i),(f=o.current)==null||f.setAttribute(D,i),a.current=i}),a}var Je=()=>{let[t,o]=r.useState(),n=x(()=>new Map);return A(()=>{n.current.forEach(a=>a()),n.current=new Map},[t]),(a,c)=>{n.current.set(a,c),o({})}},We={position:"absolute",width:"1px",height:"1px",padding:"0",margin:"-1px",overflow:"hidden",clip:"rect(0, 0, 0, 0)",whiteSpace:"nowrap",borderWidth:"0"};0&&(module.exports={Command,CommandDialog,CommandEmpty,CommandGroup,CommandInput,CommandItem,CommandList,CommandLoading,CommandRoot,CommandSeparator,commandScore,useCommandState}); +diff --git a/dist/index.mjs b/dist/index.mjs +index 798a436cba7ea1506303d72e5708be4b464797e7..0cc5cf3a06d6450b21e1e51c01d39a0486d11f18 100644 +--- a/dist/index.mjs ++++ b/dist/index.mjs +@@ -1 +1 @@ +-import*as C from"@radix-ui/react-dialog";import*as t from"react";import le from"command-score";var ue='[cmdk-list-sizer=""]',M='[cmdk-group=""]',N='[cmdk-group-items=""]',de='[cmdk-group-heading=""]',ee='[cmdk-item=""]',Z=`${ee}:not([aria-disabled="true"])`,z="cmdk-item-select",S="data-value",fe=(n,a)=>le(n,a),te=t.createContext(void 0),k=()=>t.useContext(te),re=t.createContext(void 0),U=()=>t.useContext(re),ne=t.createContext(void 0),oe=t.forwardRef((n,a)=>{let r=t.useRef(null),o=x(()=>({search:"",value:"",filtered:{count:0,items:new Map,groups:new Set}})),u=x(()=>new Set),l=x(()=>new Map),p=x(()=>new Map),f=x(()=>new Set),d=ae(n),{label:v,children:E,value:R,onValueChange:w,filter:O,shouldFilter:ie,...D}=n,F=t.useId(),g=t.useId(),A=t.useId(),y=ye();L(()=>{if(R!==void 0){let e=R.trim().toLowerCase();o.current.value=e,y(6,W),h.emit()}},[R]);let h=t.useMemo(()=>({subscribe:e=>(f.current.add(e),()=>f.current.delete(e)),snapshot:()=>o.current,setState:(e,c,i)=>{var s,m,b;if(!Object.is(o.current[e],c)){if(o.current[e]=c,e==="search")j(),G(),y(1,V);else if(e==="value")if(((s=d.current)==null?void 0:s.value)!==void 0){(b=(m=d.current).onValueChange)==null||b.call(m,c);return}else i||y(5,W);h.emit()}},emit:()=>{f.current.forEach(e=>e())}}),[]),K=t.useMemo(()=>({value:(e,c)=>{c!==p.current.get(e)&&(p.current.set(e,c),o.current.filtered.items.set(e,B(c)),y(2,()=>{G(),h.emit()}))},item:(e,c)=>(u.current.add(e),c&&(l.current.has(c)?l.current.get(c).add(e):l.current.set(c,new Set([e]))),y(3,()=>{j(),G(),o.current.value||V(),h.emit()}),()=>{p.current.delete(e),u.current.delete(e),o.current.filtered.items.delete(e),y(4,()=>{j(),V(),h.emit()})}),group:e=>(l.current.has(e)||l.current.set(e,new Set),()=>{p.current.delete(e),l.current.delete(e)}),filter:()=>d.current.shouldFilter,label:v||n["aria-label"],listId:F,inputId:A,labelId:g}),[]);function B(e){var i;let c=((i=d.current)==null?void 0:i.filter)??fe;return e?c(e,o.current.search):0}function G(){if(!r.current||!o.current.search||d.current.shouldFilter===!1)return;let e=o.current.filtered.items,c=[];o.current.filtered.groups.forEach(s=>{let m=l.current.get(s),b=0;m.forEach(P=>{let ce=e.get(P);b=Math.max(ce,b)}),c.push([s,b])});let i=r.current.querySelector(ue);I().sort((s,m)=>{let b=s.getAttribute(S),P=m.getAttribute(S);return(e.get(P)??0)-(e.get(b)??0)}).forEach(s=>{let m=s.closest(N);m?m.appendChild(s.parentElement===m?s:s.closest(`${N} > *`)):i.appendChild(s.parentElement===i?s:s.closest(`${N} > *`))}),c.sort((s,m)=>m[1]-s[1]).forEach(s=>{let m=r.current.querySelector(`${M}[${S}="${s[0]}"]`);m==null||m.parentElement.appendChild(m)})}function V(){let e=I().find(i=>!i.ariaDisabled),c=e==null?void 0:e.getAttribute(S);h.setState("value",c||void 0)}function j(){if(!o.current.search||d.current.shouldFilter===!1){o.current.filtered.count=u.current.size;return}o.current.filtered.groups=new Set;let e=0;for(let c of u.current){let i=p.current.get(c),s=B(i);o.current.filtered.items.set(c,s),s>0&&e++}for(let[c,i]of l.current)for(let s of i)if(o.current.filtered.items.get(s)>0){o.current.filtered.groups.add(c);break}o.current.filtered.count=e}function W(){var c,i,s;let e=_();e&&(((c=e.parentElement)==null?void 0:c.firstChild)===e&&((s=(i=e.closest(M))==null?void 0:i.querySelector(de))==null||s.scrollIntoView({block:"nearest"})),e.scrollIntoView({block:"nearest"}))}function _(){return r.current.querySelector(`${ee}[aria-selected="true"]`)}function I(){return Array.from(r.current.querySelectorAll(Z))}function q(e){let i=I()[e];i&&h.setState("value",i.getAttribute(S))}function $(e){var b;let c=_(),i=I(),s=i.findIndex(P=>P===c),m=i[s+e];(b=d.current)!=null&&b.loop&&(m=s+e<0?i[i.length-1]:s+e===i.length?i[0]:i[s+e]),m&&h.setState("value",m.getAttribute(S))}function J(e){let c=_(),i=c==null?void 0:c.closest(M),s;for(;i&&!s;)i=e>0?Se(i,M):Ce(i,M),s=i==null?void 0:i.querySelector(Z);s?h.setState("value",s.getAttribute(S)):$(e)}let Q=()=>q(I().length-1),X=e=>{e.preventDefault(),e.metaKey?Q():e.altKey?J(1):$(1)},Y=e=>{e.preventDefault(),e.metaKey?q(0):e.altKey?J(-1):$(-1)};return t.createElement("div",{ref:H([r,a]),...D,"cmdk-root":"",onKeyDown:e=>{var c;if((c=D.onKeyDown)==null||c.call(D,e),!e.defaultPrevented)switch(e.key){case"n":case"j":{e.ctrlKey&&X(e);break}case"ArrowDown":{X(e);break}case"p":case"k":{e.ctrlKey&&Y(e);break}case"ArrowUp":{Y(e);break}case"Home":{e.preventDefault(),q(0);break}case"End":{e.preventDefault(),Q();break}case"Enter":{e.preventDefault();let i=_();if(i){let s=new Event(z);i.dispatchEvent(s)}}}}},t.createElement("label",{"cmdk-label":"",htmlFor:K.inputId,id:K.labelId,style:xe},v),t.createElement(re.Provider,{value:h},t.createElement(te.Provider,{value:K},E)))}),me=t.forwardRef((n,a)=>{let r=t.useId(),o=t.useRef(null),u=t.useContext(ne),l=k(),p=ae(n);L(()=>l.item(r,u),[]);let f=se(r,o,[n.value,n.children,o]),d=U(),v=T(g=>g.value&&g.value===f.current),E=T(g=>l.filter()===!1?!0:g.search?g.filtered.items.get(r)>0:!0);t.useEffect(()=>{let g=o.current;if(!(!g||n.disabled))return g.addEventListener(z,R),()=>g.removeEventListener(z,R)},[E,n.onSelect,n.disabled]);function R(){var g,A;(A=(g=p.current).onSelect)==null||A.call(g,f.current)}function w(){d.setState("value",f.current,!0)}if(!E)return null;let{disabled:O,value:ie,onSelect:D,...F}=n;return t.createElement("div",{ref:H([o,a]),...F,"cmdk-item":"",role:"option","aria-disabled":O||void 0,"aria-selected":v||void 0,"data-selected":v||void 0,onPointerMove:O?void 0:w,onClick:O?void 0:R},n.children)}),pe=t.forwardRef((n,a)=>{let{heading:r,children:o,...u}=n,l=t.useId(),p=t.useRef(null),f=t.useRef(null),d=t.useId(),v=k(),E=T(w=>v.filter()===!1?!0:w.search?w.filtered.groups.has(l):!0);L(()=>v.group(l),[]),se(l,p,[n.value,n.heading,f]);let R=t.createElement(ne.Provider,{value:l},o);return t.createElement("div",{ref:H([p,a]),...u,"cmdk-group":"",role:"presentation",hidden:E?void 0:!0},r&&t.createElement("div",{ref:f,"cmdk-group-heading":"","aria-hidden":!0,id:d},r),t.createElement("div",{"cmdk-group-items":"",role:"group","aria-labelledby":r?d:void 0},R))}),ge=t.forwardRef((n,a)=>{let{alwaysRender:r,...o}=n,u=t.useRef(null),l=T(p=>!p.search);return!r&&!l?null:t.createElement("div",{ref:H([u,a]),...o,"cmdk-separator":"",role:"separator"})}),ve=t.forwardRef((n,a)=>{let{onValueChange:r,...o}=n,u=n.value!=null,l=U(),p=T(d=>d.search),f=k();return t.useEffect(()=>{n.value!=null&&l.setState("search",n.value)},[n.value]),t.createElement("input",{ref:a,...o,"cmdk-input":"",autoComplete:"off",autoCorrect:"off",spellCheck:!1,"aria-autocomplete":"list",role:"combobox","aria-expanded":!0,"aria-controls":f.listId,"aria-labelledby":f.labelId,id:f.inputId,type:"text",value:u?n.value:p,onChange:d=>{u||l.setState("search",d.target.value),r==null||r(d.target.value)}})}),Re=t.forwardRef((n,a)=>{let{children:r,...o}=n,u=t.useRef(null),l=t.useRef(null),p=k();return t.useEffect(()=>{if(l.current&&u.current){let f=l.current,d=u.current,v,E=new ResizeObserver(()=>{v=requestAnimationFrame(()=>{let R=f.getBoundingClientRect().height;d.style.setProperty("--cmdk-list-height",R.toFixed(1)+"px")})});return E.observe(f),()=>{cancelAnimationFrame(v),E.unobserve(f)}}},[]),t.createElement("div",{ref:H([u,a]),...o,"cmdk-list":"",role:"listbox","aria-label":"Suggestions",id:p.listId,"aria-labelledby":p.inputId},t.createElement("div",{ref:l,"cmdk-list-sizer":""},r))}),be=t.forwardRef((n,a)=>{let{open:r,onOpenChange:o,container:u,...l}=n;return t.createElement(C.Root,{open:r,onOpenChange:o},t.createElement(C.Portal,{container:u},t.createElement(C.Overlay,{"cmdk-overlay":""}),t.createElement(C.Content,{"aria-label":n.label,"cmdk-dialog":""},t.createElement(oe,{ref:a,...l}))))}),he=t.forwardRef((n,a)=>{let r=t.useRef(!0),o=T(u=>u.filtered.count===0);return t.useEffect(()=>{r.current=!1},[]),r.current||!o?null:t.createElement("div",{ref:a,...n,"cmdk-empty":"",role:"presentation"})}),Ee=t.forwardRef((n,a)=>{let{progress:r,children:o,...u}=n;return t.createElement("div",{ref:a,...u,"cmdk-loading":"",role:"progressbar","aria-valuenow":r,"aria-valuemin":0,"aria-valuemax":100,"aria-label":"Loading..."},t.createElement("div",{"aria-hidden":!0},o))}),Le=Object.assign(oe,{List:Re,Item:me,Input:ve,Group:pe,Separator:ge,Dialog:be,Empty:he,Loading:Ee});function Se(n,a){let r=n.nextElementSibling;for(;r;){if(r.matches(a))return r;r=r.nextElementSibling}}function Ce(n,a){let r=n.previousElementSibling;for(;r;){if(r.matches(a))return r;r=r.previousElementSibling}}function ae(n){let a=t.useRef(n);return L(()=>{a.current=n}),a}var L=typeof window>"u"?t.useEffect:t.useLayoutEffect;function x(n){let a=t.useRef();return a.current===void 0&&(a.current=n()),a}function H(n){return a=>{n.forEach(r=>{typeof r=="function"?r(a):r!=null&&(r.current=a)})}}function T(n){let a=U(),r=()=>n(a.snapshot());return t.useSyncExternalStore(a.subscribe,r,r)}function se(n,a,r){let o=t.useRef(),u=k();return L(()=>{var p;let l=(()=>{var f;for(let d of r){if(typeof d=="string")return d.trim().toLowerCase();if(typeof d=="object"&&"current"in d&&d.current)return(f=d.current.textContent)==null?void 0:f.trim().toLowerCase()}})();u.value(n,l),(p=a.current)==null||p.setAttribute(S,l),o.current=l}),o}var ye=()=>{let[n,a]=t.useState(),r=x(()=>new Map);return L(()=>{r.current.forEach(o=>o()),r.current=new Map},[n]),(o,u)=>{r.current.set(o,u),a({})}},xe={position:"absolute",width:"1px",height:"1px",padding:"0",margin:"-1px",overflow:"hidden",clip:"rect(0, 0, 0, 0)",whiteSpace:"nowrap",borderWidth:"0"};export{Le as Command,be as CommandDialog,he as CommandEmpty,pe as CommandGroup,ve as CommandInput,me as CommandItem,Re as CommandList,Ee as CommandLoading,oe as CommandRoot,ge as CommandSeparator,T as useCommandState}; ++import*as P from"@radix-ui/react-dialog";import*as t from"react";var ae=1,ge=.9,he=.8,Ee=.17,B=.1,X=.999,Ce=.9999;var Se=.99,be=/[\\\/_+.#"@\[\(\{&]/,Te=/[\\\/_+.#"@\[\(\{&]/g,ye=/[\s-]/,se=/[\s-]/g;function Y(r,a,n,o,c,i,f){if(i===a.length)return c===r.length?ae:Se;var v=`${c},${i}`;if(f[v]!==void 0)return f[v];for(var d=o.charAt(i),p=n.indexOf(d,c),R=0,m,S,T,E;p>=0;)m=Y(r,a,n,o,p+1,i+1,f),m>R&&(p===c?m*=ae:be.test(r.charAt(p-1))?(m*=he,T=r.slice(c,p-1).match(Te),T&&c>0&&(m*=Math.pow(X,T.length))):ye.test(r.charAt(p-1))?(m*=ge,E=r.slice(c,p-1).match(se),E&&c>0&&(m*=Math.pow(X,E.length))):(m*=Ee,c>0&&(m*=Math.pow(X,p-c))),r.charAt(p)!==a.charAt(i)&&(m*=Ce)),(mm&&(m=S*B)),m>R&&(R=m),p=n.indexOf(d,p+1);return f[v]=R,R}function ce(r){return r.toLowerCase().replace(se," ")}function ie(r,a){return Y(r,a,ce(r),ce(a),0,0,{})}var Pe='[cmdk-list-sizer=""]',k='[cmdk-group=""]',J='[cmdk-group-items=""]',Me='[cmdk-group-heading=""]',z='[cmdk-item=""]',le=`${z}:not([aria-disabled="true"])`,W="cmdk-item-select",L="data-value",Le=(r,a)=>ie(r,a),ue=t.createContext(void 0),I=()=>t.useContext(ue),de=t.createContext(void 0),Q=()=>t.useContext(de),fe=t.createContext(void 0),me=t.forwardRef((r,a)=>{let n=t.useRef(null),o=x(()=>{var e;return{search:"",value:r.value??((e=r.defaultValue)==null?void 0:e.toLowerCase())??"",filtered:{count:0,items:new Map,groups:new Set}}}),c=x(()=>new Set),i=x(()=>new Map),f=x(()=>new Map),v=x(()=>new Set),d=pe(r),{label:p,children:R,value:m,onValueChange:S,filter:T,shouldFilter:E,vimBindings:G=!0,...w}=r,K=t.useId(),N=t.useId(),h=t.useId(),y=Ge();A(()=>{if(m!==void 0){let e=m.trim().toLowerCase();o.current.value=e,y(6,ee),b.emit()}},[m]);let b=t.useMemo(()=>({subscribe:e=>(v.current.add(e),()=>v.current.delete(e)),snapshot:()=>o.current,setState:(e,u,s)=>{var l,g,C;if(!Object.is(o.current[e],u)){if(o.current[e]=u,e==="search")j(),F(),y(1,$);else if(e==="value")if(((l=d.current)==null?void 0:l.value)!==void 0){let M=u??"";(C=(g=d.current).onValueChange)==null||C.call(g,M);return}else s||y(5,ee);b.emit()}},emit:()=>{v.current.forEach(e=>e())}}),[]),V=t.useMemo(()=>({value:(e,u)=>{u!==f.current.get(e)&&(f.current.set(e,u),o.current.filtered.items.set(e,Z(u)),y(2,()=>{F(),b.emit()}))},item:(e,u)=>(c.current.add(e),u&&(i.current.has(u)?i.current.get(u).add(e):i.current.set(u,new Set([e]))),y(3,()=>{j(),F(),o.current.value||$(),b.emit()}),()=>{f.current.delete(e),c.current.delete(e),o.current.filtered.items.delete(e);let s=_();y(4,()=>{j(),(s==null?void 0:s.getAttribute("id"))===e&&$(),b.emit()})}),group:e=>(i.current.has(e)||i.current.set(e,new Set),()=>{f.current.delete(e),i.current.delete(e)}),filter:()=>d.current.shouldFilter,label:p||r["aria-label"],commandRef:n,listId:K,inputId:h,labelId:N}),[]);function Z(e){var s;let u=((s=d.current)==null?void 0:s.filter)??Le;return e?u(e,o.current.search):0}function F(){if(!n.current||!o.current.search||d.current.shouldFilter===!1)return;let e=o.current.filtered.items,u=[];o.current.filtered.groups.forEach(l=>{let g=i.current.get(l),C=0;g.forEach(M=>{let ve=e.get(M);C=Math.max(ve,C)}),u.push([l,C])});let s=n.current.querySelector(Pe);O().sort((l,g)=>{let C=l.getAttribute("id"),M=g.getAttribute("id");return(e.get(M)??0)-(e.get(C)??0)}).forEach(l=>{let g=l.closest(J);g?g.appendChild(l.parentElement===g?l:l.closest(`${J} > *`)):s.appendChild(l.parentElement===s?l:l.closest(`${J} > *`))}),u.sort((l,g)=>g[1]-l[1]).forEach(l=>{let g=n.current.querySelector(`${k}[${L}="${l[0]}"]`);g==null||g.parentElement.appendChild(g)})}function $(){let e=O().find(s=>!s.ariaDisabled),u=e==null?void 0:e.getAttribute(L);b.setState("value",u||void 0)}function j(){if(!o.current.search||d.current.shouldFilter===!1){o.current.filtered.count=c.current.size;return}o.current.filtered.groups=new Set;let e=0;for(let u of c.current){let s=f.current.get(u),l=Z(s);o.current.filtered.items.set(u,l),l>0&&e++}for(let[u,s]of i.current)for(let l of s)if(o.current.filtered.items.get(l)>0){o.current.filtered.groups.add(u);break}o.current.filtered.count=e}function ee(){var u,s,l;let e=_();e&&(((u=e.parentElement)==null?void 0:u.firstChild)===e&&((l=(s=e.closest(k))==null?void 0:s.querySelector(Me))==null||l.scrollIntoView({block:"nearest"})),e.scrollIntoView({block:"nearest"}))}function _(){var e;return(e=n.current)==null?void 0:e.querySelector(`${z}[aria-selected="true"]`)}function O(){return Array.from(n.current.querySelectorAll(le))}function U(e){let s=O()[e];s&&b.setState("value",s.getAttribute(L))}function q(e){var C;let u=_(),s=O(),l=s.findIndex(M=>M===u),g=s[l+e];(C=d.current)!=null&&C.loop&&(g=l+e<0?s[s.length-1]:l+e===s.length?s[0]:s[l+e]),g&&b.setState("value",g.getAttribute(L))}function te(e){let u=_(),s=u==null?void 0:u.closest(k),l;for(;s&&!l;)s=e>0?He(s,k):Ne(s,k),l=s==null?void 0:s.querySelector(le);l?b.setState("value",l.getAttribute(L)):q(e)}let re=()=>U(O().length-1),ne=e=>{e.preventDefault(),e.metaKey?re():e.altKey?te(1):q(1)},oe=e=>{e.preventDefault(),e.metaKey?U(0):e.altKey?te(-1):q(-1)};return t.createElement("div",{ref:H([n,a]),...w,"cmdk-root":"",onKeyDown:e=>{var u;if((u=w.onKeyDown)==null||u.call(w,e),!e.defaultPrevented)switch(e.key){case"n":case"j":{G&&e.ctrlKey&&ne(e);break}case"ArrowDown":{ne(e);break}case"p":case"k":{G&&e.ctrlKey&&oe(e);break}case"ArrowUp":{oe(e);break}case"Home":{e.preventDefault(),U(0);break}case"End":{e.preventDefault(),re();break}case"Enter":if(!e.nativeEvent.isComposing){e.preventDefault();let s=_();if(s){let l=new Event(W);s.dispatchEvent(l)}}}}},t.createElement("label",{"cmdk-label":"",htmlFor:V.inputId,id:V.labelId,style:Ke},p),t.createElement(de.Provider,{value:b},t.createElement(ue.Provider,{value:V},R)))}),De=t.forwardRef((r,a)=>{var N;let n=t.useId(),o=t.useRef(null),c=t.useContext(fe),i=I(),f=pe(r),v=((N=f.current)==null?void 0:N.forceMount)??(c==null?void 0:c.forceMount);A(()=>i.item(n,c==null?void 0:c.id),[]);let d=Re(n,o,[r.value,r.children,o]),p=Q(),R=D(h=>h.value&&h.value===d.current),m=D(h=>v||i.filter()===!1?!0:h.search?h.filtered.items.get(n)>0:!0);t.useEffect(()=>{let h=o.current;if(!(!h||r.disabled))return h.addEventListener(W,S),()=>h.removeEventListener(W,S)},[m,r.onSelect,r.disabled]);function S(){var h,y;T(),(y=(h=f.current).onSelect)==null||y.call(h,d.current)}function T(){p.setState("value",d.current,!0)}if(!m)return null;let{disabled:E,value:G,onSelect:w,...K}=r;return t.createElement("div",{ref:H([o,a]),...K,id:n,"cmdk-item":"",role:"option","aria-disabled":E||void 0,"aria-selected":R||void 0,"data-disabled":E||void 0,"data-selected":R||void 0,onPointerMove:E?void 0:T,onClick:E?void 0:S},r.children)}),xe=t.forwardRef((r,a)=>{let{heading:n,children:o,forceMount:c,...i}=r,f=t.useId(),v=t.useRef(null),d=t.useRef(null),p=t.useId(),R=I(),m=D(E=>c||R.filter()===!1?!0:E.search?E.filtered.groups.has(f):!0);A(()=>R.group(f),[]),Re(f,v,[r.value,r.heading,d]);let S=t.useMemo(()=>({id:f,forceMount:c}),[c]),T=t.createElement(fe.Provider,{value:S},o);return t.createElement("div",{ref:H([v,a]),...i,"cmdk-group":"",role:"presentation",hidden:m?void 0:!0},n&&t.createElement("div",{ref:d,"cmdk-group-heading":"","aria-hidden":!0,id:p},n),t.createElement("div",{"cmdk-group-items":"",role:"group","aria-labelledby":n?p:void 0},T))}),Ae=t.forwardRef((r,a)=>{let{alwaysRender:n,...o}=r,c=t.useRef(null),i=D(f=>!f.search);return!n&&!i?null:t.createElement("div",{ref:H([c,a]),...o,"cmdk-separator":"",role:"separator"})}),we=t.forwardRef((r,a)=>{let{onValueChange:n,...o}=r,c=r.value!=null,i=Q(),f=D(R=>R.search),v=D(R=>R.value),d=I(),p=t.useMemo(()=>{var m;let R=(m=d.commandRef.current)==null?void 0:m.querySelector(`${z}[${L}="${v}"]`);return R==null?void 0:R.getAttribute("id")},[v,d.commandRef]);return t.useEffect(()=>{r.value!=null&&i.setState("search",r.value)},[r.value]),t.createElement("input",{ref:a,...o,"cmdk-input":"",autoComplete:"off",autoCorrect:"off",spellCheck:!1,"aria-autocomplete":"list",role:"combobox","aria-expanded":!0,"aria-controls":d.listId,"aria-labelledby":d.labelId,"aria-activedescendant":p,id:d.inputId,type:"text",value:c?r.value:f,onChange:R=>{c||i.setState("search",R.target.value),n==null||n(R.target.value)}})}),_e=t.forwardRef((r,a)=>{let{children:n,...o}=r,c=t.useRef(null),i=t.useRef(null),f=I();return t.useEffect(()=>{if(i.current&&c.current){let v=i.current,d=c.current,p,R=new ResizeObserver(()=>{p=requestAnimationFrame(()=>{let m=v.offsetHeight;d.style.setProperty("--cmdk-list-height",m.toFixed(1)+"px")})});return R.observe(v),()=>{cancelAnimationFrame(p),R.unobserve(v)}}},[]),t.createElement("div",{ref:H([c,a]),...o,"cmdk-list":"",role:"listbox","aria-label":"Suggestions",id:f.listId,"aria-labelledby":f.inputId},t.createElement("div",{ref:i,"cmdk-list-sizer":""},n))}),Oe=t.forwardRef((r,a)=>{let{open:n,onOpenChange:o,overlayClassName:c,contentClassName:i,container:f,...v}=r;return t.createElement(P.Root,{open:n,onOpenChange:o},t.createElement(P.Portal,{container:f},t.createElement(P.Overlay,{"cmdk-overlay":"",className:c}),t.createElement(P.Content,{"aria-label":r.label,"cmdk-dialog":"",className:i},t.createElement(me,{ref:a,...v}))))}),ke=t.forwardRef((r,a)=>{let n=t.useRef(!0),o=D(c=>c.filtered.count===0);return t.useEffect(()=>{n.current=!1},[]),n.current||!o?null:t.createElement("div",{ref:a,...r,"cmdk-empty":"",role:"presentation"})}),Ie=t.forwardRef((r,a)=>{let{progress:n,children:o,...c}=r;return t.createElement("div",{ref:a,...c,"cmdk-loading":"",role:"progressbar","aria-valuenow":n,"aria-valuemin":0,"aria-valuemax":100,"aria-label":"Loading..."},t.createElement("div",{"aria-hidden":!0},o))}),$e=Object.assign(me,{List:_e,Item:De,Input:we,Group:xe,Separator:Ae,Dialog:Oe,Empty:ke,Loading:Ie});function He(r,a){let n=r.nextElementSibling;for(;n;){if(n.matches(a))return n;n=n.nextElementSibling}}function Ne(r,a){let n=r.previousElementSibling;for(;n;){if(n.matches(a))return n;n=n.previousElementSibling}}function pe(r){let a=t.useRef(r);return A(()=>{a.current=r}),a}var A=typeof window>"u"?t.useEffect:t.useLayoutEffect;function x(r){let a=t.useRef();return a.current===void 0&&(a.current=r()),a}function H(r){return a=>{r.forEach(n=>{typeof n=="function"?n(a):n!=null&&(n.current=a)})}}function D(r){let a=Q(),n=()=>r(a.snapshot());return t.useSyncExternalStore(a.subscribe,n,n)}function Re(r,a,n){let o=t.useRef(),c=I();return A(()=>{var f;let i=(()=>{var v;for(let d of n){if(typeof d=="string")return d.trim().toLowerCase();if(typeof d=="object"&&"current"in d)return d.current?(v=d.current.textContent)==null?void 0:v.trim().toLowerCase():o.current}})();c.value(r,i),(f=a.current)==null||f.setAttribute(L,i),o.current=i}),o}var Ge=()=>{let[r,a]=t.useState(),n=x(()=>new Map);return A(()=>{n.current.forEach(o=>o()),n.current=new Map},[r]),(o,c)=>{n.current.set(o,c),a({})}},Ke={position:"absolute",width:"1px",height:"1px",padding:"0",margin:"-1px",overflow:"hidden",clip:"rect(0, 0, 0, 0)",whiteSpace:"nowrap",borderWidth:"0"};export{$e as Command,Oe as CommandDialog,ke as CommandEmpty,xe as CommandGroup,we as CommandInput,De as CommandItem,_e as CommandList,Ie as CommandLoading,me as CommandRoot,Ae as CommandSeparator,ie as commandScore,D as useCommandState}; diff --git a/package.json b/package.json index ec60f92b62..dd8bbf264b 100644 --- a/package.json +++ b/package.json @@ -172,6 +172,7 @@ "next-auth@^4.24.5": "patch:next-auth@npm%3A4.24.5#~/.yarn/patches/next-auth-npm-4.24.5-8428e11927.patch", "@reforged/maker-appimage/@electron-forge/maker-base": "7.2.0", "macos-alias": "npm:macos-alias-building@latest", - "fs-xattr": "npm:@napi-rs/xattr@latest" + "fs-xattr": "npm:@napi-rs/xattr@latest", + "@radix-ui/react-dialog": "npm:@radix-ui/react-dialog@latest" } } diff --git a/packages/common/cmdk/LICENSE.md b/packages/common/cmdk/LICENSE.md deleted file mode 100644 index 76228ccc3a..0000000000 --- a/packages/common/cmdk/LICENSE.md +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2022 Paco Coursey - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/packages/common/cmdk/README.md b/packages/common/cmdk/README.md deleted file mode 100644 index f4a7a1fa38..0000000000 --- a/packages/common/cmdk/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# copied directly from https://github.com/pacocoursey/cmdk - -will remove after a new CMDK version is published to npm diff --git a/packages/common/cmdk/package.json b/packages/common/cmdk/package.json deleted file mode 100644 index b7017dcd7e..0000000000 --- a/packages/common/cmdk/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "@affine/cmdk", - "private": true, - "type": "module", - "main": "./src/index.tsx", - "module": "./src/index.tsx", - "devDependencies": { - "react": "18.2.0", - "react-dom": "18.2.0" - }, - "dependencies": { - "@radix-ui/react-dialog": "^1.0.5" - }, - "version": "0.11.0" -} diff --git a/packages/common/cmdk/src/command-score.ts b/packages/common/cmdk/src/command-score.ts deleted file mode 100644 index ee4eff7d7c..0000000000 --- a/packages/common/cmdk/src/command-score.ts +++ /dev/null @@ -1,179 +0,0 @@ -/* eslint-disable */ -// @ts-nocheck - -// The scores are arranged so that a continuous match of characters will -// result in a total score of 1. -// -// The best case, this character is a match, and either this is the start -// of the string, or the previous character was also a match. -var SCORE_CONTINUE_MATCH = 1, - // A new match at the start of a word scores better than a new match - // elsewhere as it's more likely that the user will type the starts - // of fragments. - // NOTE: We score word jumps between spaces slightly higher than slashes, brackets - // hyphens, etc. - SCORE_SPACE_WORD_JUMP = 0.9, - SCORE_NON_SPACE_WORD_JUMP = 0.8, - // Any other match isn't ideal, but we include it for completeness. - SCORE_CHARACTER_JUMP = 0.17, - // If the user transposed two letters, it should be significantly penalized. - // - // i.e. "ouch" is more likely than "curtain" when "uc" is typed. - SCORE_TRANSPOSITION = 0.1, - // The goodness of a match should decay slightly with each missing - // character. - // - // i.e. "bad" is more likely than "bard" when "bd" is typed. - // - // This will not change the order of suggestions based on SCORE_* until - // 100 characters are inserted between matches. - PENALTY_SKIPPED = 0.999, - // The goodness of an exact-case match should be higher than a - // case-insensitive match by a small amount. - // - // i.e. "HTML" is more likely than "haml" when "HM" is typed. - // - // This will not change the order of suggestions based on SCORE_* until - // 1000 characters are inserted between matches. - PENALTY_CASE_MISMATCH = 0.9999, - // Match higher for letters closer to the beginning of the word - PENALTY_DISTANCE_FROM_START = 0.9, - // If the word has more characters than the user typed, it should - // be penalised slightly. - // - // i.e. "html" is more likely than "html5" if I type "html". - // - // However, it may well be the case that there's a sensible secondary - // ordering (like alphabetical) that it makes sense to rely on when - // there are many prefix matches, so we don't make the penalty increase - // with the number of tokens. - PENALTY_NOT_COMPLETE = 0.99; - -var IS_GAP_REGEXP = /[\\\/_+.#"@\[\(\{&]/, - COUNT_GAPS_REGEXP = /[\\\/_+.#"@\[\(\{&]/g, - IS_SPACE_REGEXP = /[\s-]/, - COUNT_SPACE_REGEXP = /[\s-]/g; - -function commandScoreInner( - string, - abbreviation, - lowerString, - lowerAbbreviation, - stringIndex, - abbreviationIndex, - memoizedResults -) { - if (abbreviationIndex === abbreviation.length) { - if (stringIndex === string.length) { - return SCORE_CONTINUE_MATCH; - } - return PENALTY_NOT_COMPLETE; - } - - var memoizeKey = `${stringIndex},${abbreviationIndex}`; - if (memoizedResults[memoizeKey] !== undefined) { - return memoizedResults[memoizeKey]; - } - - var abbreviationChar = lowerAbbreviation.charAt(abbreviationIndex); - var index = lowerString.indexOf(abbreviationChar, stringIndex); - var highScore = 0; - - var score, transposedScore, wordBreaks, spaceBreaks; - - while (index >= 0) { - score = commandScoreInner( - string, - abbreviation, - lowerString, - lowerAbbreviation, - index + 1, - abbreviationIndex + 1, - memoizedResults - ); - if (score > highScore) { - if (index === stringIndex) { - score *= SCORE_CONTINUE_MATCH; - } else if (IS_GAP_REGEXP.test(string.charAt(index - 1))) { - score *= SCORE_NON_SPACE_WORD_JUMP; - wordBreaks = string - .slice(stringIndex, index - 1) - .match(COUNT_GAPS_REGEXP); - if (wordBreaks && stringIndex > 0) { - score *= Math.pow(PENALTY_SKIPPED, wordBreaks.length); - } - } else if (IS_SPACE_REGEXP.test(string.charAt(index - 1))) { - score *= SCORE_SPACE_WORD_JUMP; - spaceBreaks = string - .slice(stringIndex, index - 1) - .match(COUNT_SPACE_REGEXP); - if (spaceBreaks && stringIndex > 0) { - score *= Math.pow(PENALTY_SKIPPED, spaceBreaks.length); - } - } else { - score *= SCORE_CHARACTER_JUMP; - if (stringIndex > 0) { - score *= Math.pow(PENALTY_SKIPPED, index - stringIndex); - } - } - - if (string.charAt(index) !== abbreviation.charAt(abbreviationIndex)) { - score *= PENALTY_CASE_MISMATCH; - } - } - - if ( - (score < SCORE_TRANSPOSITION && - lowerString.charAt(index - 1) === - lowerAbbreviation.charAt(abbreviationIndex + 1)) || - (lowerAbbreviation.charAt(abbreviationIndex + 1) === - lowerAbbreviation.charAt(abbreviationIndex) && // allow duplicate letters. Ref #7428 - lowerString.charAt(index - 1) !== - lowerAbbreviation.charAt(abbreviationIndex)) - ) { - transposedScore = commandScoreInner( - string, - abbreviation, - lowerString, - lowerAbbreviation, - index + 1, - abbreviationIndex + 2, - memoizedResults - ); - - if (transposedScore * SCORE_TRANSPOSITION > score) { - score = transposedScore * SCORE_TRANSPOSITION; - } - } - - if (score > highScore) { - highScore = score; - } - - index = lowerString.indexOf(abbreviationChar, index + 1); - } - - memoizedResults[memoizeKey] = highScore; - return highScore; -} - -function formatInput(string) { - // convert all valid space characters to space so they match each other - return string.toLowerCase().replace(COUNT_SPACE_REGEXP, ' '); -} - -export function commandScore(string: string, abbreviation: string): number { - /* NOTE: - * in the original, we used to do the lower-casing on each recursive call, but this meant that toLowerCase() - * was the dominating cost in the algorithm, passing both is a little ugly, but considerably faster. - */ - return commandScoreInner( - string, - abbreviation, - formatInput(string), - formatInput(abbreviation), - 0, - 0, - {} - ); -} diff --git a/packages/common/cmdk/src/index.tsx b/packages/common/cmdk/src/index.tsx deleted file mode 100644 index e8549a4384..0000000000 --- a/packages/common/cmdk/src/index.tsx +++ /dev/null @@ -1,1151 +0,0 @@ -/* eslint-disable */ -// @ts-nocheck -import * as RadixDialog from '@radix-ui/react-dialog'; -import * as React from 'react'; -import { commandScore } from './command-score'; - -type Children = { - children?: React.ReactNode; -}; -type DivProps = React.HTMLAttributes; - -type LoadingProps = Children & - DivProps & { - /** Estimated progress of loading asynchronous options. */ - progress?: number; - }; -type EmptyProps = Children & DivProps & {}; -type SeparatorProps = DivProps & { - /** Whether this separator should always be rendered. Useful if you disable automatic filtering. */ - alwaysRender?: boolean; -}; -type DialogProps = RadixDialog.DialogProps & - CommandProps & { - /** Provide a className to the Dialog overlay. */ - overlayClassName?: string; - /** Provide a className to the Dialog content. */ - contentClassName?: string; - /** Provide a custom element the Dialog should portal into. */ - container?: HTMLElement; - }; -type ListProps = Children & DivProps & {}; -type ItemProps = Children & - Omit & { - /** Whether this item is currently disabled. */ - disabled?: boolean; - /** Event handler for when this item is selected, either via click or keyboard selection. */ - onSelect?: (value: string) => void; - /** - * A unique value for this item. - * If no value is provided, it will be inferred from `children` or the rendered `textContent`. If your `textContent` changes between renders, you _must_ provide a stable, unique `value`. - */ - value?: string; - /** Whether this item is forcibly rendered regardless of filtering. */ - forceMount?: boolean; - }; -type GroupProps = Children & - Omit & { - /** Optional heading to render for this group. */ - heading?: React.ReactNode; - /** If no heading is provided, you must provide a value that is unique for this group. */ - value?: string; - /** Whether this group is forcibly rendered regardless of filtering. */ - forceMount?: boolean; - }; -type InputProps = Omit< - React.InputHTMLAttributes, - 'value' | 'onChange' | 'type' -> & { - /** - * Optional controlled state for the value of the search input. - */ - value?: string; - /** - * Event handler called when the search value changes. - */ - onValueChange?: (search: string) => void; -}; -type CommandProps = Children & - DivProps & { - /** - * Accessible label for this command menu. Not shown visibly. - */ - label?: string; - /** - * Optionally set to `false` to turn off the automatic filtering and sorting. - * If `false`, you must conditionally render valid items based on the search query yourself. - */ - shouldFilter?: boolean; - /** - * Custom filter function for whether each command menu item should matches the given search query. - * It should return a number between 0 and 1, with 1 being the best match and 0 being hidden entirely. - * By default, uses the `command-score` library. - */ - filter?: (value: string, search: string) => number; - /** - * Optional default item value when it is initially rendered. - */ - defaultValue?: string; - /** - * Optional controlled state of the selected command menu item. - */ - value?: string; - /** - * Event handler called when the selected item of the menu changes. - */ - onValueChange?: (value: string) => void; - /** - * Optionally set to `true` to turn on looping around when using the arrow keys. - */ - loop?: boolean; - /** - * Set to `false` to disable ctrl+n/j/p/k shortcuts. Defaults to `true`. - */ - vimBindings?: boolean; - }; - -type Context = { - value: (id: string, value: string) => void; - item: (id: string, groupId: string) => () => void; - group: (id: string) => () => void; - filter: () => boolean; - label: string; - commandRef: React.RefObject; - // Ids - listId: string; - labelId: string; - inputId: string; -}; -type State = { - search: string; - value: string; - filtered: { - count: number; - items: Map; - groups: Set; - }; -}; -type Store = { - subscribe: (callback: () => void) => () => void; - snapshot: () => State; - setState: ( - key: K, - value: State[K], - opts?: any - ) => void; - emit: () => void; -}; -type Group = { - id: string; - forceMount?: boolean; -}; - -const LIST_SELECTOR = `[cmdk-list-sizer=""]`; -const GROUP_SELECTOR = `[cmdk-group=""]`; -const GROUP_ITEMS_SELECTOR = `[cmdk-group-items=""]`; -const GROUP_HEADING_SELECTOR = `[cmdk-group-heading=""]`; -const ITEM_SELECTOR = `[cmdk-item=""]`; -const VALID_ITEM_SELECTOR = `${ITEM_SELECTOR}:not([aria-disabled="true"])`; -const SELECT_EVENT = `cmdk-item-select`; -const VALUE_ATTR = `data-value`; -const defaultFilter: CommandProps['filter'] = (value, search) => - commandScore(value, search); - -// @ts-ignore -const CommandContext = React.createContext(undefined); -const useCommand = () => React.useContext(CommandContext); -// @ts-ignore -const StoreContext = React.createContext(undefined); -const useStore = () => React.useContext(StoreContext); -// @ts-ignore -const GroupContext = React.createContext(undefined); - -const Command = React.forwardRef( - (props, forwardedRef) => { - const ref = React.useRef(null); - const state = useLazyRef(() => ({ - /** Value of the search query. */ - search: '', - /** Currently selected item value. */ - value: props.value ?? props.defaultValue?.toLowerCase() ?? '', - filtered: { - /** The count of all visible items. */ - count: 0, - /** Map from visible item id to its search score. */ - items: new Map(), - /** Set of groups with at least one visible item. */ - groups: new Set(), - }, - })); - const allItems = useLazyRef>(() => new Set()); // [...itemIds] - const allGroups = useLazyRef>>(() => new Map()); // groupId → [...itemIds] - const ids = useLazyRef>(() => new Map()); // id → value - const listeners = useLazyRef void>>(() => new Set()); // [...rerenders] - const propsRef = useAsRef(props); - const { - label, - children, - value, - onValueChange, - filter, - shouldFilter, - vimBindings = true, - ...etc - } = props; - - const listId = React.useId(); - const labelId = React.useId(); - const inputId = React.useId(); - - const schedule = useScheduleLayoutEffect(); - - /** Controlled mode `value` handling. */ - useLayoutEffect(() => { - if (value !== undefined) { - const v = value.trim().toLowerCase(); - state.current.value = v; - schedule(6, scrollSelectedIntoView); - store.emit(); - } - }, [value]); - - const store: Store = React.useMemo(() => { - return { - subscribe: cb => { - listeners.current.add(cb); - return () => listeners.current.delete(cb); - }, - snapshot: () => { - return state.current; - }, - setState: (key, value, opts) => { - if (Object.is(state.current[key], value)) return; - state.current[key] = value; - - if (key === 'search') { - // Filter synchronously before emitting back to children - filterItems(); - sort(); - schedule(1, selectFirstItem); - } else if (key === 'value') { - if (propsRef.current?.value !== undefined) { - // If controlled, just call the callback instead of updating state internally - const newValue = (value ?? '') as string; - propsRef.current.onValueChange?.(newValue); - return; - // opts is a boolean referring to whether it should NOT be scrolled into view - } else if (!opts) { - // Scroll the selected item into view - schedule(5, scrollSelectedIntoView); - } - } - - // Notify subscribers that state has changed - store.emit(); - }, - emit: () => { - listeners.current.forEach(l => l()); - }, - }; - }, []); - - const context: Context = React.useMemo( - () => ({ - // Keep id → value mapping up-to-date - value: (id, value) => { - if (value !== ids.current.get(id)) { - ids.current.set(id, value); - state.current.filtered.items.set(id, score(value)); - schedule(2, () => { - sort(); - store.emit(); - }); - } - }, - // Track item lifecycle (mount, unmount) - item: (id, groupId) => { - allItems.current.add(id); - - // Track this item within the group - if (groupId) { - if (!allGroups.current.has(groupId)) { - allGroups.current.set(groupId, new Set([id])); - } else { - allGroups.current.get(groupId).add(id); - } - } - - // Batch this, multiple items can mount in one pass - // and we should not be filtering/sorting/emitting each time - schedule(3, () => { - filterItems(); - sort(); - - // Could be initial mount, select the first item if none already selected - if (!state.current.value) { - selectFirstItem(); - } - - store.emit(); - }); - - return () => { - ids.current.delete(id); - allItems.current.delete(id); - state.current.filtered.items.delete(id); - const selectedItem = getSelectedItem(); - - // Batch this, multiple items could be removed in one pass - schedule(4, () => { - filterItems(); - - // The item removed have been the selected one, - // so selection should be moved to the first - if (selectedItem?.getAttribute('id') === id) selectFirstItem(); - - store.emit(); - }); - }; - }, - // Track group lifecycle (mount, unmount) - group: id => { - if (!allGroups.current.has(id)) { - allGroups.current.set(id, new Set()); - } - - return () => { - ids.current.delete(id); - allGroups.current.delete(id); - }; - }, - filter: () => { - return propsRef.current.shouldFilter; - }, - label: label || props['aria-label'], - commandRef: ref, - listId, - inputId, - labelId, - }), - [] - ); - - function score(value: string) { - const filter = propsRef.current?.filter ?? defaultFilter; - return value ? filter(value, state.current.search) : 0; - } - - /** Sorts items by score, and groups by highest item score. */ - function sort() { - if ( - !ref.current || - !state.current.search || - // Explicitly false, because true | undefined is the default - propsRef.current.shouldFilter === false - ) { - return; - } - - const scores = state.current.filtered.items; - - // Sort the groups - const groups: [string, number][] = []; - state.current.filtered.groups.forEach(value => { - const items = allGroups.current.get(value); - - // Get the maximum score of the group's items - let max = 0; - items.forEach(item => { - const score = scores.get(item); - max = Math.max(score, max); - }); - - groups.push([value, max]); - }); - - // Sort items within groups to bottom - // Sort items outside of groups - // Sort groups to bottom (pushes all non-grouped items to the top) - const list = ref.current.querySelector(LIST_SELECTOR); - - // Sort the items - getValidItems() - .sort((a, b) => { - const valueA = a.getAttribute('id'); - const valueB = b.getAttribute('id'); - return (scores.get(valueB) ?? 0) - (scores.get(valueA) ?? 0); - }) - .forEach(item => { - const group = item.closest(GROUP_ITEMS_SELECTOR); - - if (group) { - group.appendChild( - item.parentElement === group - ? item - : item.closest(`${GROUP_ITEMS_SELECTOR} > *`) - ); - } else { - list.appendChild( - item.parentElement === list - ? item - : item.closest(`${GROUP_ITEMS_SELECTOR} > *`) - ); - } - }); - - groups - .sort((a, b) => b[1] - a[1]) - .forEach(group => { - const element = ref.current.querySelector( - `${GROUP_SELECTOR}[${VALUE_ATTR}="${group[0]}"]` - ); - element?.parentElement.appendChild(element); - }); - } - - function selectFirstItem() { - const item = getValidItems().find(item => !item.ariaDisabled); - const value = item?.getAttribute(VALUE_ATTR); - store.setState('value', value || undefined); - } - - /** Filters the current items. */ - function filterItems() { - if ( - !state.current.search || - // Explicitly false, because true | undefined is the default - propsRef.current.shouldFilter === false - ) { - state.current.filtered.count = allItems.current.size; - // Do nothing, each item will know to show itself because search is empty - return; - } - - // Reset the groups - state.current.filtered.groups = new Set(); - let itemCount = 0; - - // Check which items should be included - for (const id of allItems.current) { - const value = ids.current.get(id); - const rank = score(value); - state.current.filtered.items.set(id, rank); - if (rank > 0) itemCount++; - } - - // Check which groups have at least 1 item shown - for (const [groupId, group] of allGroups.current) { - for (const itemId of group) { - if (state.current.filtered.items.get(itemId) > 0) { - state.current.filtered.groups.add(groupId); - break; - } - } - } - - state.current.filtered.count = itemCount; - } - - function scrollSelectedIntoView() { - const item = getSelectedItem(); - - if (item) { - if (item.parentElement?.firstChild === item) { - // First item in Group, ensure heading is in view - item - .closest(GROUP_SELECTOR) - ?.querySelector(GROUP_HEADING_SELECTOR) - ?.scrollIntoView({ block: 'nearest' }); - } - - // Ensure the item is always in view - item.scrollIntoView({ block: 'nearest' }); - } - } - - /** Getters */ - - function getSelectedItem() { - return ref.current?.querySelector( - `${ITEM_SELECTOR}[aria-selected="true"]` - ); - } - - function getValidItems() { - return Array.from(ref.current.querySelectorAll(VALID_ITEM_SELECTOR)); - } - - /** Setters */ - - function updateSelectedToIndex(index: number) { - const items = getValidItems(); - const item = items[index]; - if (item) store.setState('value', item.getAttribute(VALUE_ATTR)); - } - - function updateSelectedByChange(change: 1 | -1) { - const selected = getSelectedItem(); - const items = getValidItems(); - const index = items.findIndex(item => item === selected); - - // Get item at this index - let newSelected = items[index + change]; - - if (propsRef.current?.loop) { - newSelected = - index + change < 0 - ? items[items.length - 1] - : index + change === items.length - ? items[0] - : items[index + change]; - } - - if (newSelected) - store.setState('value', newSelected.getAttribute(VALUE_ATTR)); - } - - function updateSelectedToGroup(change: 1 | -1) { - const selected = getSelectedItem(); - let group = selected?.closest(GROUP_SELECTOR); - let item: HTMLElement; - - while (group && !item) { - group = - change > 0 - ? findNextSibling(group, GROUP_SELECTOR) - : findPreviousSibling(group, GROUP_SELECTOR); - item = group?.querySelector(VALID_ITEM_SELECTOR); - } - - if (item) { - store.setState('value', item.getAttribute(VALUE_ATTR)); - } else { - updateSelectedByChange(change); - } - } - - const last = () => updateSelectedToIndex(getValidItems().length - 1); - - const next = (e: React.KeyboardEvent) => { - e.preventDefault(); - - if (e.metaKey) { - // Last item - last(); - } else if (e.altKey) { - // Next group - updateSelectedToGroup(1); - } else { - // Next item - updateSelectedByChange(1); - } - }; - - const prev = (e: React.KeyboardEvent) => { - e.preventDefault(); - - if (e.metaKey) { - // First item - updateSelectedToIndex(0); - } else if (e.altKey) { - // Previous group - updateSelectedToGroup(-1); - } else { - // Previous item - updateSelectedByChange(-1); - } - }; - - return ( -
{ - etc.onKeyDown?.(e); - - if (!e.defaultPrevented) { - switch (e.key) { - case 'n': - case 'j': { - // vim keybind down - if (vimBindings && e.ctrlKey) { - next(e); - } - break; - } - case 'ArrowDown': { - next(e); - break; - } - case 'p': - case 'k': { - // vim keybind up - if (vimBindings && e.ctrlKey) { - prev(e); - } - break; - } - case 'ArrowUp': { - prev(e); - break; - } - case 'Home': { - // First item - e.preventDefault(); - updateSelectedToIndex(0); - break; - } - case 'End': { - // Last item - e.preventDefault(); - last(); - break; - } - case 'Enter': { - // Check if IME composition is finished before triggering onSelect - // This prevents unwanted triggering while user is still inputting text with IME - if (!e.nativeEvent.isComposing) { - // Trigger item onSelect - e.preventDefault(); - const item = getSelectedItem(); - if (item) { - const event = new Event(SELECT_EVENT); - item.dispatchEvent(event); - } - } - } - } - } - }} - > - - - - {children} - - -
- ); - } -); - -/** - * Command menu item. Becomes active on pointer enter or through keyboard navigation. - * Preferably pass a `value`, otherwise the value will be inferred from `children` or - * the rendered item's `textContent`. - */ -const Item = React.forwardRef( - (props, forwardedRef) => { - const id = React.useId(); - const ref = React.useRef(null); - const groupContext = React.useContext(GroupContext); - const context = useCommand(); - const propsRef = useAsRef(props); - const forceMount = propsRef.current?.forceMount ?? groupContext?.forceMount; - - useLayoutEffect(() => { - return context.item(id, groupContext?.id); - }, []); - - const value = useValue(id, ref, [props.value, props.children, ref]); - - const store = useStore(); - const selected = useCmdk( - state => state.value && state.value === value.current - ); - const render = useCmdk(state => - forceMount - ? true - : context.filter() === false - ? true - : !state.search - ? true - : state.filtered.items.get(id) > 0 - ); - - React.useEffect(() => { - const element = ref.current; - if (!element || props.disabled) return; - element.addEventListener(SELECT_EVENT, onSelect); - return () => element.removeEventListener(SELECT_EVENT, onSelect); - }, [render, props.onSelect, props.disabled]); - - function onSelect() { - select(); - propsRef.current.onSelect?.(value.current); - } - - function select() { - store.setState('value', value.current, true); - } - - if (!render) return null; - - const { disabled, value: _, onSelect: __, ...etc } = props; - - return ( -
- {props.children} -
- ); - } -); - -/** - * Group command menu items together with a heading. - * Grouped items are always shown together. - */ -const Group = React.forwardRef( - (props, forwardedRef) => { - const { heading, children, forceMount, ...etc } = props; - const id = React.useId(); - const ref = React.useRef(null); - const headingRef = React.useRef(null); - const headingId = React.useId(); - const context = useCommand(); - const render = useCmdk(state => - forceMount - ? true - : context.filter() === false - ? true - : !state.search - ? true - : state.filtered.groups.has(id) - ); - - useLayoutEffect(() => { - return context.group(id); - }, []); - - useValue(id, ref, [props.value, props.heading, headingRef]); - - const contextValue = React.useMemo( - () => ({ id, forceMount }), - [forceMount] - ); - const inner = ( - - {children} - - ); - - return ( - - ); - } -); - -/** - * A visual and semantic separator between items or groups. - * Visible when the search query is empty or `alwaysRender` is true, hidden otherwise. - */ -const Separator = React.forwardRef( - (props, forwardedRef) => { - const { alwaysRender, ...etc } = props; - const ref = React.useRef(null); - const render = useCmdk(state => !state.search); - - if (!alwaysRender && !render) return null; - return ( -
- ); - } -); - -/** - * Command menu input. - * All props are forwarded to the underyling `input` element. - */ -const Input = React.forwardRef( - (props, forwardedRef) => { - const { onValueChange, ...etc } = props; - const isControlled = props.value != null; - const store = useStore(); - const search = useCmdk(state => state.search); - const value = useCmdk(state => state.value); - const context = useCommand(); - - const selectedItemId = React.useMemo(() => { - const item = context.commandRef.current?.querySelector( - `${ITEM_SELECTOR}[${VALUE_ATTR}="${value}"]` - ); - return item?.getAttribute('id'); - }, [value, context.commandRef]); - - React.useEffect(() => { - if (props.value != null) { - store.setState('search', props.value); - } - }, [props.value]); - - return ( - { - if ( - e.key === 'Escape' || - e.key === 'k' || - e.key.startsWith('Arrow') || - e.key === 'Enter' - ) { - return; - } - e.stopPropagation(); - }} - autoComplete="off" - autoCorrect="off" - spellCheck={false} - aria-autocomplete="list" - role="combobox" - aria-expanded={true} - aria-controls={context.listId} - aria-labelledby={context.labelId} - aria-activedescendant={selectedItemId} - id={context.inputId} - type="text" - value={isControlled ? props.value : search} - onChange={e => { - if (!isControlled) { - store.setState('search', e.target.value); - } - - onValueChange?.(e.target.value); - }} - /> - ); - } -); - -/** - * Contains `Item`, `Group`, and `Separator`. - * Use the `--cmdk-list-height` CSS variable to animate height based on the number of results. - */ -const List = React.forwardRef( - (props, forwardedRef) => { - const { children, ...etc } = props; - const ref = React.useRef(null); - const height = React.useRef(null); - const context = useCommand(); - - React.useEffect(() => { - if (height.current && ref.current) { - const el = height.current; - const wrapper = ref.current; - let animationFrame; - const observer = new ResizeObserver(() => { - animationFrame = requestAnimationFrame(() => { - const height = el.offsetHeight; - wrapper.style.setProperty( - `--cmdk-list-height`, - height.toFixed(1) + 'px' - ); - }); - }); - observer.observe(el); - return () => { - cancelAnimationFrame(animationFrame); - observer.unobserve(el); - }; - } - }, []); - - return ( -
-
- {children} -
-
- ); - } -); - -/** - * Renders the command menu in a Radix Dialog. - */ -const Dialog = React.forwardRef( - (props, forwardedRef) => { - const { - open, - onOpenChange, - overlayClassName, - contentClassName, - container, - ...etc - } = props; - return ( - - - - - - - - - ); - } -); - -/** - * Automatically renders when there are no results for the search query. - */ -const Empty = React.forwardRef( - (props, forwardedRef) => { - const isFirstRender = React.useRef(true); - const render = useCmdk(state => state.filtered.count === 0); - - React.useEffect(() => { - isFirstRender.current = false; - }, []); - - if (isFirstRender.current || !render) return null; - return ( -
- ); - } -); - -/** - * You should conditionally render this with `progress` while loading asynchronous items. - */ -const Loading = React.forwardRef( - (props, forwardedRef) => { - const { progress, children, ...etc } = props; - - return ( -
-
{children}
-
- ); - } -); - -const pkg = Object.assign(Command, { - List, - Item, - Input, - Group, - Separator, - Dialog, - Empty, - Loading, -}); - -export { useCmdk as useCommandState }; -export { pkg as Command }; - -export { Command as CommandRoot }; -export { List as CommandList }; -export { Item as CommandItem }; -export { Input as CommandInput }; -export { Group as CommandGroup }; -export { Separator as CommandSeparator }; -export { Dialog as CommandDialog }; -export { Empty as CommandEmpty }; -export { Loading as CommandLoading }; -export { commandScore }; - -/** - * - * - * Helpers - * - * - */ - -function findNextSibling(el: Element, selector: string) { - let sibling = el.nextElementSibling; - - while (sibling) { - if (sibling.matches(selector)) return sibling; - sibling = sibling.nextElementSibling; - } -} - -function findPreviousSibling(el: Element, selector: string) { - let sibling = el.previousElementSibling; - - while (sibling) { - if (sibling.matches(selector)) return sibling; - sibling = sibling.previousElementSibling; - } -} - -function useAsRef(data: T) { - const ref = React.useRef(data); - - useLayoutEffect(() => { - ref.current = data; - }); - - return ref; -} - -const useLayoutEffect = - typeof window === 'undefined' ? React.useEffect : React.useLayoutEffect; - -function useLazyRef(fn: () => T) { - const ref = React.useRef(); - - if (ref.current === undefined) { - ref.current = fn(); - } - - return ref as React.MutableRefObject; -} - -// ESM is still a nightmare with Next.js so I'm just gonna copy the package code in -// https://github.com/gregberge/react-merge-refs -// Copyright (c) 2020 Greg Bergé -function mergeRefs( - refs: Array | React.LegacyRef> -): React.RefCallback { - return value => { - refs.forEach(ref => { - if (typeof ref === 'function') { - ref(value); - } else if (ref != null) { - (ref as React.MutableRefObject).current = value; - } - }); - }; -} - -/** Run a selector against the store state. */ -function useCmdk(selector: (state: State) => T) { - const store = useStore(); - const cb = () => selector(store.snapshot()); - return React.useSyncExternalStore(store.subscribe, cb, cb); -} - -function useValue( - id: string, - ref: React.RefObject, - deps: (string | React.ReactNode | React.RefObject)[] -) { - const valueRef = React.useRef(); - const context = useCommand(); - - useLayoutEffect(() => { - const value = (() => { - for (const part of deps) { - if (typeof part === 'string') { - return part.trim().toLowerCase(); - } - - if (typeof part === 'object' && 'current' in part) { - if (part.current) { - return part.current.textContent?.trim().toLowerCase(); - } - return valueRef.current; - } - } - })(); - - context.value(id, value); - ref.current?.setAttribute(VALUE_ATTR, value); - valueRef.current = value; - }); - - return valueRef; -} - -/** Imperatively run a function on the next layout effect cycle. */ -const useScheduleLayoutEffect = () => { - const [s, ss] = React.useState(); - const fns = useLazyRef(() => new Map void>()); - - useLayoutEffect(() => { - fns.current.forEach(f => f()); - fns.current = new Map(); - }, [s]); - - return (id: string | number, cb: () => void) => { - fns.current.set(id, cb); - ss({}); - }; -}; - -const srOnlyStyles = { - position: 'absolute', - width: '1px', - height: '1px', - padding: '0', - margin: '-1px', - overflow: 'hidden', - clip: 'rect(0, 0, 0, 0)', - whiteSpace: 'nowrap', - borderWidth: '0', -} as const; diff --git a/packages/common/cmdk/tsconfig.json b/packages/common/cmdk/tsconfig.json deleted file mode 100644 index 4bbd8d0b79..0000000000 --- a/packages/common/cmdk/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../../../tsconfig.json", - "include": ["./src"], - "compilerOptions": { - "composite": true, - "noEmit": false, - "outDir": "lib" - } -} diff --git a/packages/frontend/core/package.json b/packages/frontend/core/package.json index 5056d8176d..77e22c8d82 100644 --- a/packages/frontend/core/package.json +++ b/packages/frontend/core/package.json @@ -17,7 +17,6 @@ }, "dependencies": { "@affine-test/fixtures": "workspace:*", - "@affine/cmdk": "workspace:*", "@affine/component": "workspace:*", "@affine/debug": "workspace:*", "@affine/electron-api": "workspace:*", @@ -55,6 +54,7 @@ "async-call-rpc": "^6.3.1", "bytes": "^3.1.2", "clsx": "^2.0.0", + "cmdk": "patch:cmdk@npm%3A0.2.0#~/.yarn/patches/cmdk-npm-0.2.0-302237a911.patch", "css-spring": "^4.1.0", "cssnano": "^6.0.1", "dayjs": "^1.11.10", diff --git a/packages/frontend/core/src/components/pure/cmdk/data.tsx b/packages/frontend/core/src/components/pure/cmdk/data.tsx index d37d08aeb7..0bec9f4ac8 100644 --- a/packages/frontend/core/src/components/pure/cmdk/data.tsx +++ b/packages/frontend/core/src/components/pure/cmdk/data.tsx @@ -1,4 +1,3 @@ -import { commandScore } from '@affine/cmdk'; import { useCollectionManager } from '@affine/component/page-list'; import { useBlockSuitePageMeta, @@ -19,6 +18,7 @@ import { type CommandCategory, PreconditionStrategy, } from '@toeverything/infra/command'; +import { commandScore } from 'cmdk'; import { atom, useAtomValue } from 'jotai'; import { groupBy } from 'lodash-es'; import { useCallback, useEffect, useMemo, useState } from 'react'; diff --git a/packages/frontend/core/src/components/pure/cmdk/main.tsx b/packages/frontend/core/src/components/pure/cmdk/main.tsx index ced65792eb..4a28ca08d3 100644 --- a/packages/frontend/core/src/components/pure/cmdk/main.tsx +++ b/packages/frontend/core/src/components/pure/cmdk/main.tsx @@ -1,11 +1,10 @@ -import { Command } from '@affine/cmdk'; -import { useCommandState } from '@affine/cmdk'; import { formatDate } from '@affine/component/page-list'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import type { PageMeta } from '@blocksuite/store'; import type { CommandCategory } from '@toeverything/infra/command'; import clsx from 'clsx'; +import { Command, useCommandState } from 'cmdk'; import { useAtom, useAtomValue } from 'jotai'; import { Suspense, @@ -225,17 +224,6 @@ export const CMDKContainer = ({ className={clsx(className, styles.panelContainer)} value={value} onValueChange={setValue} - // Handle KeyboardEvent conflicts with blocksuite - onKeyDown={(e: React.KeyboardEvent) => { - if ( - e.key === 'ArrowDown' || - e.key === 'ArrowUp' || - e.key === 'ArrowLeft' || - e.key === 'ArrowRight' - ) { - e.stopPropagation(); - } - }} > {/* todo: add page context here */} {isInEditor ? ( diff --git a/packages/frontend/core/src/components/pure/cmdk/not-found.tsx b/packages/frontend/core/src/components/pure/cmdk/not-found.tsx index d1c2c80078..fd96315339 100644 --- a/packages/frontend/core/src/components/pure/cmdk/not-found.tsx +++ b/packages/frontend/core/src/components/pure/cmdk/not-found.tsx @@ -1,5 +1,5 @@ -import { useCommandState } from '@affine/cmdk'; import { SearchIcon } from '@blocksuite/icons'; +import { useCommandState } from 'cmdk'; import { useAtomValue } from 'jotai'; import { cmdkQueryAtom } from './data'; diff --git a/tsconfig.json b/tsconfig.json index 0a9b0cee9d..120becff91 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -107,9 +107,6 @@ "path": "./packages/frontend/workspace-impl" }, // Common - { - "path": "./packages/common/cmdk" - }, { "path": "./packages/common/debug" }, diff --git a/yarn.lock b/yarn.lock index 5c7e013baf..d4ba009b01 100644 --- a/yarn.lock +++ b/yarn.lock @@ -187,16 +187,6 @@ __metadata: languageName: unknown linkType: soft -"@affine/cmdk@workspace:*, @affine/cmdk@workspace:packages/common/cmdk": - version: 0.0.0-use.local - resolution: "@affine/cmdk@workspace:packages/common/cmdk" - dependencies: - "@radix-ui/react-dialog": "npm:^1.0.5" - react: "npm:18.2.0" - react-dom: "npm:18.2.0" - languageName: unknown - linkType: soft - "@affine/commitlint-config@workspace:tools/commitlint": version: 0.0.0-use.local resolution: "@affine/commitlint-config@workspace:tools/commitlint" @@ -308,7 +298,6 @@ __metadata: resolution: "@affine/core@workspace:packages/frontend/core" dependencies: "@affine-test/fixtures": "workspace:*" - "@affine/cmdk": "workspace:*" "@affine/component": "workspace:*" "@affine/debug": "workspace:*" "@affine/electron-api": "workspace:*" @@ -359,6 +348,7 @@ __metadata: async-call-rpc: "npm:^6.3.1" bytes: "npm:^3.1.2" clsx: "npm:^2.0.0" + cmdk: "patch:cmdk@npm%3A0.2.0#~/.yarn/patches/cmdk-npm-0.2.0-302237a911.patch" copy-webpack-plugin: "npm:^11.0.0" css-loader: "npm:^6.8.1" css-spring: "npm:^4.1.0" @@ -9422,7 +9412,7 @@ __metadata: languageName: node linkType: hard -"@radix-ui/react-dialog@npm:^1.0.4, @radix-ui/react-dialog@npm:^1.0.5": +"@radix-ui/react-dialog@npm:@radix-ui/react-dialog@latest": version: 1.0.5 resolution: "@radix-ui/react-dialog@npm:1.0.5" dependencies: @@ -17092,6 +17082,32 @@ __metadata: languageName: node linkType: hard +"cmdk@npm:0.2.0": + version: 0.2.0 + resolution: "cmdk@npm:0.2.0" + dependencies: + "@radix-ui/react-dialog": "npm:1.0.0" + command-score: "npm:0.1.2" + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + checksum: e178e3d3276e0b5fd158c9c99716c0405427871f48fa97c15c4be2de24be4a478cf0205ffa04244628dbe103dd8573a1bd1aa68f04f8b60633d4ffc04e5eee62 + languageName: node + linkType: hard + +"cmdk@patch:cmdk@npm%3A0.2.0#~/.yarn/patches/cmdk-npm-0.2.0-302237a911.patch": + version: 0.2.0 + resolution: "cmdk@patch:cmdk@npm%3A0.2.0#~/.yarn/patches/cmdk-npm-0.2.0-302237a911.patch::version=0.2.0&hash=640d85" + dependencies: + "@radix-ui/react-dialog": "npm:1.0.0" + command-score: "npm:0.1.2" + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + checksum: 758bacb7761a72c6fa03a1b20ea2514ff14ad6b3d00cc1d8bc6781a216b0a719f991eacded9f923ddcf1b58b8efb304209b268c17bd7d6f5671aa3352934b754 + languageName: node + linkType: hard + "co@npm:^4.6.0": version: 4.6.0 resolution: "co@npm:4.6.0" @@ -17237,6 +17253,13 @@ __metadata: languageName: node linkType: hard +"command-score@npm:0.1.2": + version: 0.1.2 + resolution: "command-score@npm:0.1.2" + checksum: 84f6a69e6b215d3fc8c9ed402d109587f511e4cc84cd5da10a7857b50fb1638953e32dcce8ed8f3549b0bfe499e82601fb7fb6891c9c71b48933d4bb8bac238a + languageName: node + linkType: hard + "commander@npm:11.0.0": version: 11.0.0 resolution: "commander@npm:11.0.0" From a59fe1b49e44e5b128d8de098b198c527d2ac1ce Mon Sep 17 00:00:00 2001 From: DarkSky Date: Wed, 10 Jan 2024 07:28:46 +0000 Subject: [PATCH 02/92] feat: adapted user quota for member api (#5521) fix AFF-494 TOV-337 --- .../server/src/modules/quota/storage.ts | 2 + .../modules/workspaces/resolvers/workspace.ts | 151 ++++++++---------- packages/backend/server/tests/mailer.spec.ts | 29 +++- 3 files changed, 94 insertions(+), 88 deletions(-) diff --git a/packages/backend/server/src/modules/quota/storage.ts b/packages/backend/server/src/modules/quota/storage.ts index f754c6576c..dde3620626 100644 --- a/packages/backend/server/src/modules/quota/storage.ts +++ b/packages/backend/server/src/modules/quota/storage.ts @@ -22,6 +22,8 @@ export class QuotaManagementService { expiredAt: quota.expiredAt, blobLimit: quota.feature.blobLimit, storageQuota: quota.feature.storageQuota, + historyPeriod: quota.feature.historyPeriod, + memberLimit: quota.feature.memberLimit, }; } diff --git a/packages/backend/server/src/modules/workspaces/resolvers/workspace.ts b/packages/backend/server/src/modules/workspaces/resolvers/workspace.ts index ad3208b555..abd7a9c522 100644 --- a/packages/backend/server/src/modules/workspaces/resolvers/workspace.ts +++ b/packages/backend/server/src/modules/workspaces/resolvers/workspace.ts @@ -1,5 +1,6 @@ import { ForbiddenException, + HttpStatus, InternalServerErrorException, Logger, NotFoundException, @@ -16,6 +17,7 @@ import { } from '@nestjs/graphql'; import type { User } from '@prisma/client'; import { getStreamAsBuffer } from 'get-stream'; +import { GraphQLError } from 'graphql'; import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs'; import { applyUpdate, Doc } from 'yjs'; @@ -26,6 +28,7 @@ import type { FileUpload } from '../../../types'; import { Auth, CurrentUser, Public } from '../../auth'; import { MailService } from '../../auth/mailer'; import { AuthService } from '../../auth/service'; +import { QuotaManagementService } from '../../quota'; import { WorkspaceBlobStorage } from '../../storage'; import { UsersService, UserType } from '../../users'; import { PermissionService } from '../permission'; @@ -54,6 +57,7 @@ export class WorkspaceResolver { private readonly mailer: MailService, private readonly prisma: PrismaService, private readonly permissions: PermissionService, + private readonly quota: QuotaManagementService, private readonly users: UsersService, private readonly event: EventEmitter, private readonly blobStorage: WorkspaceBlobStorage @@ -321,8 +325,23 @@ export class WorkspaceResolver { throw new ForbiddenException('Cannot change owner'); } - const target = await this.users.findUserByEmail(email); + // member limit check + const [memberCount, quota] = await Promise.all([ + this.prisma.workspaceUserPermission.count({ + where: { workspaceId }, + }), + this.quota.getUserQuota(user.id), + ]); + if (memberCount >= quota.memberLimit) { + throw new GraphQLError('Workspace member limit reached', { + extensions: { + status: HttpStatus[HttpStatus.PAYLOAD_TOO_LARGE], + code: HttpStatus.PAYLOAD_TOO_LARGE, + }, + }); + } + let target = await this.users.findUserByEmail(email); if (target) { const originRecord = await this.prisma.workspaceUserPermission.findFirst({ where: { @@ -330,94 +349,52 @@ export class WorkspaceResolver { userId: target.id, }, }); - - if (originRecord) { - return originRecord.id; - } - - const inviteId = await this.permissions.grant( - workspaceId, - target.id, - permission - ); - if (sendInviteMail) { - const inviteInfo = await this.getInviteInfo(inviteId); - - try { - await this.mailer.sendInviteEmail(email, inviteId, { - workspace: { - id: inviteInfo.workspace.id, - name: inviteInfo.workspace.name, - avatar: inviteInfo.workspace.avatar, - }, - user: { - avatar: inviteInfo.user?.avatarUrl || '', - name: inviteInfo.user?.name || '', - }, - }); - } catch (e) { - const ret = await this.permissions.revokeWorkspace( - workspaceId, - target.id - ); - - if (!ret) { - this.logger.fatal( - `failed to send ${workspaceId} invite email to ${email} and failed to revoke permission: ${inviteId}, ${e}` - ); - } else { - this.logger.warn( - `failed to send ${workspaceId} invite email to ${email}, but successfully revoked permission: ${e}` - ); - } - - return new InternalServerErrorException(e); - } - } - return inviteId; + // only invite if the user is not already in the workspace + if (originRecord) return originRecord.id; } else { - const user = await this.auth.createAnonymousUser(email); - const inviteId = await this.permissions.grant( - workspaceId, - user.id, - permission - ); - if (sendInviteMail) { - const inviteInfo = await this.getInviteInfo(inviteId); - - try { - await this.mailer.sendInviteEmail(email, inviteId, { - workspace: { - id: inviteInfo.workspace.id, - name: inviteInfo.workspace.name, - avatar: inviteInfo.workspace.avatar, - }, - user: { - avatar: inviteInfo.user?.avatarUrl || '', - name: inviteInfo.user?.name || '', - }, - }); - } catch (e) { - const ret = await this.permissions.revokeWorkspace( - workspaceId, - user.id - ); - - if (!ret) { - this.logger.fatal( - `failed to send ${workspaceId} invite email to ${email} and failed to revoke permission: ${inviteId}, ${e}` - ); - } else { - this.logger.warn( - `failed to send ${workspaceId} invite email to ${email}, but successfully revoked permission: ${e}` - ); - } - - return new InternalServerErrorException(e); - } - } - return inviteId; + target = await this.auth.createAnonymousUser(email); } + + const inviteId = await this.permissions.grant( + workspaceId, + target.id, + permission + ); + if (sendInviteMail) { + const inviteInfo = await this.getInviteInfo(inviteId); + + try { + await this.mailer.sendInviteEmail(email, inviteId, { + workspace: { + id: inviteInfo.workspace.id, + name: inviteInfo.workspace.name, + avatar: inviteInfo.workspace.avatar, + }, + user: { + avatar: inviteInfo.user?.avatarUrl || '', + name: inviteInfo.user?.name || '', + }, + }); + } catch (e) { + const ret = await this.permissions.revokeWorkspace( + workspaceId, + target.id + ); + + if (!ret) { + this.logger.fatal( + `failed to send ${workspaceId} invite email to ${email} and failed to revoke permission: ${inviteId}, ${e}` + ); + } else { + this.logger.warn( + `failed to send ${workspaceId} invite email to ${email}, but successfully revoked permission: ${e}` + ); + } + + return new InternalServerErrorException(e); + } + } + return inviteId; } @Throttle({ diff --git a/packages/backend/server/tests/mailer.spec.ts b/packages/backend/server/tests/mailer.spec.ts index 023cfdf35d..e25355bf16 100644 --- a/packages/backend/server/tests/mailer.spec.ts +++ b/packages/backend/server/tests/mailer.spec.ts @@ -9,7 +9,8 @@ import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs'; import { AppModule } from '../src/app'; import { MailService } from '../src/modules/auth/mailer'; -import { FeatureManagementService } from '../src/modules/features'; +import { FeatureKind, FeatureManagementService } from '../src/modules/features'; +import { Quotas } from '../src/modules/quota'; import { PrismaService } from '../src/prisma'; import { createWorkspace, getInviteInfo, inviteUser, signUp } from './utils'; @@ -88,6 +89,32 @@ const FakePrisma = { }, }; }, + get features() { + return { + async findFirst() { + return { + id: 1, + type: FeatureKind.Quota, + feature: Quotas[0].feature, + configs: Quotas[0].configs, + version: Quotas[0].version, + createdAt: new Date(), + }; + }, + }; + }, + get userFeatures() { + return { + async findFirst() { + return { + createdAt: new Date(), + featureId: 1, + reason: '', + expiredAt: null, + }; + }, + }; + }, }; const test = ava as TestFn<{ From 0d7ffb05113162992d0e126162bb9a1f8cdbf222 Mon Sep 17 00:00:00 2001 From: DarkSky Date: Wed, 10 Jan 2024 07:28:52 +0000 Subject: [PATCH 03/92] feat: add unlimited workspace support (#5523) fix AFF-505 --- .../server/src/modules/features/feature.ts | 13 +++++++ .../src/modules/features/types/common.ts | 1 + .../src/modules/features/types/index.ts | 15 +++++++- .../features/types/unlimited-workspace.ts | 8 +++++ .../modules/workspaces/resolvers/workspace.ts | 36 +++++++++++-------- packages/backend/server/src/schema.gql | 1 + packages/backend/server/tests/mailer.spec.ts | 6 +++- 7 files changed, 64 insertions(+), 16 deletions(-) create mode 100644 packages/backend/server/src/modules/features/types/unlimited-workspace.ts diff --git a/packages/backend/server/src/modules/features/feature.ts b/packages/backend/server/src/modules/features/feature.ts index 9f585ed99d..3945790b66 100644 --- a/packages/backend/server/src/modules/features/feature.ts +++ b/packages/backend/server/src/modules/features/feature.ts @@ -42,9 +42,22 @@ export class EarlyAccessFeatureConfig extends FeatureConfig { } } +export class UnlimitedWorkspaceFeatureConfig extends FeatureConfig { + override config!: Feature & { feature: FeatureType.UnlimitedWorkspace }; + + constructor(data: any) { + super(data); + + if (this.config.feature !== FeatureType.UnlimitedWorkspace) { + throw new Error('Invalid feature config: type is not EarlyAccess'); + } + } +} + const FeatureConfigMap = { [FeatureType.Copilot]: CopilotFeatureConfig, [FeatureType.EarlyAccess]: EarlyAccessFeatureConfig, + [FeatureType.UnlimitedWorkspace]: UnlimitedWorkspaceFeatureConfig, }; export type FeatureConfigType = InstanceType< diff --git a/packages/backend/server/src/modules/features/types/common.ts b/packages/backend/server/src/modules/features/types/common.ts index 913705567b..3095b49e0f 100644 --- a/packages/backend/server/src/modules/features/types/common.ts +++ b/packages/backend/server/src/modules/features/types/common.ts @@ -3,6 +3,7 @@ import { registerEnumType } from '@nestjs/graphql'; export enum FeatureType { Copilot = 'copilot', EarlyAccess = 'early_access', + UnlimitedWorkspace = 'unlimited_workspace', } registerEnumType(FeatureType, { diff --git a/packages/backend/server/src/modules/features/types/index.ts b/packages/backend/server/src/modules/features/types/index.ts index 0fd8b62323..0f82a393aa 100644 --- a/packages/backend/server/src/modules/features/types/index.ts +++ b/packages/backend/server/src/modules/features/types/index.ts @@ -3,6 +3,7 @@ import { z } from 'zod'; import { FeatureType } from './common'; import { featureCopilot } from './copilot'; import { featureEarlyAccess } from './early-access'; +import { featureUnlimitedWorkspace } from './unlimited-workspace'; /// ======== common schema ======== @@ -43,6 +44,12 @@ export const Features: Feature[] = [ version: 2, configs: {}, }, + { + feature: FeatureType.UnlimitedWorkspace, + type: FeatureKind.Feature, + version: 1, + configs: {}, + }, ]; /// ======== schema infer ======== @@ -51,7 +58,13 @@ export const FeatureSchema = commonFeatureSchema .extend({ type: z.literal(FeatureKind.Feature), }) - .and(z.discriminatedUnion('feature', [featureCopilot, featureEarlyAccess])); + .and( + z.discriminatedUnion('feature', [ + featureCopilot, + featureEarlyAccess, + featureUnlimitedWorkspace, + ]) + ); export type Feature = z.infer; diff --git a/packages/backend/server/src/modules/features/types/unlimited-workspace.ts b/packages/backend/server/src/modules/features/types/unlimited-workspace.ts new file mode 100644 index 0000000000..b9b471e9e1 --- /dev/null +++ b/packages/backend/server/src/modules/features/types/unlimited-workspace.ts @@ -0,0 +1,8 @@ +import { z } from 'zod'; + +import { FeatureType } from './common'; + +export const featureUnlimitedWorkspace = z.object({ + feature: z.literal(FeatureType.UnlimitedWorkspace), + configs: z.object({}), +}); diff --git a/packages/backend/server/src/modules/workspaces/resolvers/workspace.ts b/packages/backend/server/src/modules/workspaces/resolvers/workspace.ts index abd7a9c522..806952155b 100644 --- a/packages/backend/server/src/modules/workspaces/resolvers/workspace.ts +++ b/packages/backend/server/src/modules/workspaces/resolvers/workspace.ts @@ -28,6 +28,7 @@ import type { FileUpload } from '../../../types'; import { Auth, CurrentUser, Public } from '../../auth'; import { MailService } from '../../auth/mailer'; import { AuthService } from '../../auth/service'; +import { FeatureManagementService, FeatureType } from '../../features'; import { QuotaManagementService } from '../../quota'; import { WorkspaceBlobStorage } from '../../storage'; import { UsersService, UserType } from '../../users'; @@ -57,6 +58,7 @@ export class WorkspaceResolver { private readonly mailer: MailService, private readonly prisma: PrismaService, private readonly permissions: PermissionService, + private readonly feature: FeatureManagementService, private readonly quota: QuotaManagementService, private readonly users: UsersService, private readonly event: EventEmitter, @@ -325,20 +327,26 @@ export class WorkspaceResolver { throw new ForbiddenException('Cannot change owner'); } - // member limit check - const [memberCount, quota] = await Promise.all([ - this.prisma.workspaceUserPermission.count({ - where: { workspaceId }, - }), - this.quota.getUserQuota(user.id), - ]); - if (memberCount >= quota.memberLimit) { - throw new GraphQLError('Workspace member limit reached', { - extensions: { - status: HttpStatus[HttpStatus.PAYLOAD_TOO_LARGE], - code: HttpStatus.PAYLOAD_TOO_LARGE, - }, - }); + const unlimited = await this.feature.hasWorkspaceFeature( + workspaceId, + FeatureType.UnlimitedWorkspace + ); + if (!unlimited) { + // member limit check + const [memberCount, quota] = await Promise.all([ + this.prisma.workspaceUserPermission.count({ + where: { workspaceId }, + }), + this.quota.getUserQuota(user.id), + ]); + if (memberCount >= quota.memberLimit) { + throw new GraphQLError('Workspace member limit reached', { + extensions: { + status: HttpStatus[HttpStatus.PAYLOAD_TOO_LARGE], + code: HttpStatus.PAYLOAD_TOO_LARGE, + }, + }); + } } let target = await this.users.findUserByEmail(email); diff --git a/packages/backend/server/src/schema.gql b/packages/backend/server/src/schema.gql index 949bea6851..c16ac448b9 100644 --- a/packages/backend/server/src/schema.gql +++ b/packages/backend/server/src/schema.gql @@ -152,6 +152,7 @@ type WorkspaceType { enum FeatureType { Copilot EarlyAccess + UnlimitedWorkspace } type InvitationWorkspaceType { diff --git a/packages/backend/server/tests/mailer.spec.ts b/packages/backend/server/tests/mailer.spec.ts index e25355bf16..1e1da0c2ef 100644 --- a/packages/backend/server/tests/mailer.spec.ts +++ b/packages/backend/server/tests/mailer.spec.ts @@ -129,7 +129,11 @@ test.beforeEach(async t => { .overrideProvider(PrismaService) .useValue(FakePrisma) .overrideProvider(FeatureManagementService) - .useValue({}) + .useValue({ + hasWorkspaceFeature() { + return false; + }, + }) .compile(); const app = module.createNestApplication(); app.use( From 237722f7f90375847dbf25383f5ce9ddab24ac5b Mon Sep 17 00:00:00 2001 From: LongYinan Date: Wed, 10 Jan 2024 08:35:21 +0000 Subject: [PATCH 04/92] feat: support self-host docker build (#5506) Test command: `docker compose -f ./.github/deployment/self-host/compose.yaml up` --- .github/deployment/front/Dockerfile | 2 +- .github/deployment/node/Dockerfile | 1 + .github/deployment/self-host/compose.yaml | 57 ++++++++++++++ .../charts/graphql/templates/deployment.yaml | 2 + .../helm/affine/charts/graphql/values.yaml | 1 + .github/workflows/deploy.yml | 77 +++++++++++++------ packages/backend/server/package.json | 2 + packages/backend/server/src/app.ts | 4 +- packages/backend/server/src/config/def.ts | 1 + packages/backend/server/src/config/default.ts | 7 +- .../server/src/metrics/opentelemetry.ts | 4 + .../src/modules/auth/next-auth-options.ts | 2 +- .../src/modules/auth/next-auth.controller.ts | 8 +- packages/backend/server/src/modules/index.ts | 8 +- packages/backend/server/src/prelude.ts | 14 +++- packages/backend/server/src/storage/index.ts | 4 +- packages/frontend/core/.webpack/config.ts | 5 +- .../workspace-impl/src/cloud/sync/index.ts | 3 +- scripts/download-blocksuite-fonts.mjs | 29 +++++++ yarn.lock | 47 ++++++++++- 20 files changed, 241 insertions(+), 37 deletions(-) create mode 100644 .github/deployment/self-host/compose.yaml create mode 100644 scripts/download-blocksuite-fonts.mjs diff --git a/.github/deployment/front/Dockerfile b/.github/deployment/front/Dockerfile index 2aa4d6913d..33a2670347 100644 --- a/.github/deployment/front/Dockerfile +++ b/.github/deployment/front/Dockerfile @@ -1,6 +1,6 @@ FROM openresty/openresty:1.21.4.3-0-buster WORKDIR /app -COPY ./packages/frontend/core/dist ./dist +COPY ./packages/frontend/core/dist/index.html ./dist/index.html COPY ./.github/deployment/front/nginx.conf /usr/local/openresty/nginx/conf/nginx.conf COPY ./.github/deployment/front/affine.nginx.conf /etc/nginx/conf.d/affine.nginx.conf diff --git a/.github/deployment/node/Dockerfile b/.github/deployment/node/Dockerfile index a8f8a2c507..b2a0d442bb 100644 --- a/.github/deployment/node/Dockerfile +++ b/.github/deployment/node/Dockerfile @@ -1,6 +1,7 @@ FROM node:18-bookworm-slim COPY ./packages/backend/server /app +COPY ./packages/frontend/core/dist /app/static WORKDIR /app RUN apt-get update && \ diff --git a/.github/deployment/self-host/compose.yaml b/.github/deployment/self-host/compose.yaml new file mode 100644 index 0000000000..cf10de9318 --- /dev/null +++ b/.github/deployment/self-host/compose.yaml @@ -0,0 +1,57 @@ +services: + affine: + image: ghcr.io/toeverything/affine-graphql:beta + container_name: affine + command: + [ + 'sh', + '-c', + './node_modules/.bin/dotenv -e /root/.affine/.env -- npm run predeploy && node --es-module-specifier-resolution=node ./dist/index.js', + ] + ports: + - '3010:3010' + - '5555:5555' + depends_on: + redis: + condition: service_healthy + postgres: + condition: service_healthy + volumes: + - ~/.affine/storage:/root/.affine/storage + - ~/.affine/.env:/root/.affine/.env + logging: + driver: 'json-file' + options: + max-size: '1000m' + restart: unless-stopped + environment: + - DISABLE_TELEMETRY=true + - NODE_ENV=production + - SERVER_FLAVOR=selfhosted + redis: + image: redis + container_name: redis + restart: unless-stopped + volumes: + - ~/.affine/redis:/data + healthcheck: + test: ['CMD', 'redis-cli', '--raw', 'incr', 'ping'] + interval: 10s + timeout: 5s + retries: 5 + postgres: + image: postgres + container_name: postgres + restart: unless-stopped + volumes: + - ~/.affine/postgres:/var/lib/postgresql/data + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U affine'] + interval: 10s + timeout: 5s + retries: 5 + environment: + POSTGRES_USER: affine + POSTGRES_PASSWORD: affine + POSTGRES_DB: affine + PGDATA: /var/lib/postgresql/data/pgdata diff --git a/.github/helm/affine/charts/graphql/templates/deployment.yaml b/.github/helm/affine/charts/graphql/templates/deployment.yaml index 38e53c3427..5452a8564b 100644 --- a/.github/helm/affine/charts/graphql/templates/deployment.yaml +++ b/.github/helm/affine/charts/graphql/templates/deployment.yaml @@ -73,6 +73,8 @@ spec: value: "{{ .Values.app.path }}" - name: AFFINE_SERVER_HOST value: "{{ .Values.app.host }}" + - name: AFFINE_SERVER_HOST + value: "{{ .Values.app.https }}" - name: ENABLE_R2_OBJECT_STORAGE value: "{{ .Values.app.objectStorage.r2.enabled }}" - name: ENABLE_CAPTCHA diff --git a/.github/helm/affine/charts/graphql/values.yaml b/.github/helm/affine/charts/graphql/values.yaml index 629997382d..b0e787d2bf 100644 --- a/.github/helm/affine/charts/graphql/values.yaml +++ b/.github/helm/affine/charts/graphql/values.yaml @@ -16,6 +16,7 @@ app: path: '' # AFFINE_SERVER_HOST host: '0.0.0.0' + https: true doc: mergeInterval: "3000" jwt: diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 39185531b9..bc598c818b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -29,6 +29,7 @@ jobs: uses: ./.github/actions/setup-node with: electron-install: false + extra-flags: workspaces focus @affine/server - name: Build Server run: yarn workspace @affine/server build - name: Upload server dist @@ -62,6 +63,7 @@ jobs: SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }} + PERFSEE_TOKEN: ${{ secrets.PERFSEE_TOKEN }} - name: Upload core artifact uses: actions/upload-artifact@v4 with: @@ -69,10 +71,10 @@ jobs: path: ./packages/frontend/core/dist if-no-files-found: error - build-storage: - name: Build Storage + build-core-selfhost: + name: Build @affine/core runs-on: ubuntu-latest - + environment: ${{ github.event.inputs.flavor }} steps: - uses: actions/checkout@v4 - name: Setup Version @@ -80,22 +82,33 @@ jobs: uses: ./.github/actions/setup-version - name: Setup Node.js uses: ./.github/actions/setup-node - - name: Build Rust - uses: ./.github/actions/build-rust - with: - target: 'x86_64-unknown-linux-gnu' - package: '@affine/storage' - nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} - - name: Upload storage.node + - name: Build Core + run: yarn nx build @affine/core --skip-nx-cache + env: + BUILD_TYPE: ${{ github.event.inputs.flavor }} + SHOULD_REPORT_TRACE: false + PUBLIC_PATH: '/' + - name: Download selfhost fonts + run: node ./scripts/download-blocksuite-fonts.mjs + - name: Upload core artifact uses: actions/upload-artifact@v4 with: - name: storage.node - path: ./packages/backend/storage/storage.node + name: selfhost-core + path: ./packages/frontend/core/dist if-no-files-found: error - build-storage-arm64: - name: Build Storage arm64 + build-storage: + name: Build Storage - ${{ matrix.targets.name }} runs-on: ubuntu-latest + strategy: + matrix: + targets: + - name: x86_64-unknown-linux-gnu + file: storage.node + - name: aarch64-unknown-linux-gnu + file: storage.arm64.node + - name: armv7-unknown-linux-gnueabihf + file: storage.armv7.node steps: - uses: actions/checkout@v4 @@ -104,16 +117,19 @@ jobs: uses: ./.github/actions/setup-version - name: Setup Node.js uses: ./.github/actions/setup-node + with: + electron-install: false + extra-flags: workspaces focus @affine/storage - name: Build Rust uses: ./.github/actions/build-rust with: - target: 'aarch64-unknown-linux-gnu' + target: ${{ matrix.targets.name }} package: '@affine/storage' nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} - - name: Upload storage.node + - name: Upload ${{ matrix.targets.file }} uses: actions/upload-artifact@v4 with: - name: storage.arm64.node + name: ${{ matrix.targets.file }} path: ./packages/backend/storage/storage.node if-no-files-found: error @@ -123,8 +139,8 @@ jobs: needs: - build-server - build-core + - build-core-selfhost - build-storage - - build-storage-arm64 steps: - uses: actions/checkout@v4 - name: Download core artifact @@ -147,8 +163,15 @@ jobs: with: name: storage.arm64.node path: ./packages/backend/storage - - name: move storage.arm64.node - run: mv ./packages/backend/storage/storage.node ./packages/backend/server/storage.arm64.node + - name: Download storage.node arm64 + uses: actions/download-artifact@v4 + with: + name: storage.armv7.node + path: . + - name: move storage files + run: | + mv ./packages/backend/storage/storage.node ./packages/backend/server/storage.arm64.node + mv storage.node ./packages/backend/server/storage.armv7.node - name: Setup env run: | echo "GIT_SHORT_HASH=$(git rev-parse --short HEAD)" >> "$GITHUB_ENV" @@ -190,9 +213,19 @@ jobs: registry-url: https://npm.pkg.github.com scope: '@toeverything' + - name: Remove core dist + run: rm -rf ./packages/frontend/core/dist + + - name: Download selfhost core artifact + uses: actions/download-artifact@v4 + with: + name: selfhost-core + path: ./packages/frontend/core/dist + - name: Install Node.js dependencies run: | - yarn config set --json supportedArchitectures.cpu '["x64", "arm64"]' + yarn config set --json supportedArchitectures.cpu '["x64", "arm64", "arm"]' + yarn config set --json supportedArchitectures.libc '["glibc"]' yarn workspaces focus @affine/server --production - name: Generate Prisma client @@ -204,7 +237,7 @@ jobs: context: . push: true pull: true - platforms: linux/amd64,linux/arm64 + platforms: linux/amd64,linux/arm64,linux/arm/v7 provenance: true file: .github/deployment/node/Dockerfile tags: ghcr.io/toeverything/affine-graphql:${{env.RELEASE_FLAVOR}}-${{ env.GIT_SHORT_HASH }},ghcr.io/toeverything/affine-graphql:${{env.RELEASE_FLAVOR}} diff --git a/packages/backend/server/package.json b/packages/backend/server/package.json index 7856cd5a7c..bc479fa867 100644 --- a/packages/backend/server/package.json +++ b/packages/backend/server/package.json @@ -32,6 +32,7 @@ "@nestjs/platform-express": "^10.2.10", "@nestjs/platform-socket.io": "^10.2.10", "@nestjs/schedule": "^4.0.0", + "@nestjs/serve-static": "^4.0.0", "@nestjs/throttler": "^5.0.1", "@nestjs/websockets": "^10.2.10", "@node-rs/argon2": "^1.5.2", @@ -57,6 +58,7 @@ "@socket.io/redis-adapter": "^8.2.1", "cookie-parser": "^1.4.6", "dotenv": "^16.3.1", + "dotenv-cli": "^7.3.0", "express": "^4.18.2", "file-type": "^19.0.0", "get-stream": "^8.0.1", diff --git a/packages/backend/server/src/app.ts b/packages/backend/server/src/app.ts index 89a4019800..ad97741aac 100644 --- a/packages/backend/server/src/app.ts +++ b/packages/backend/server/src/app.ts @@ -3,7 +3,7 @@ import { APP_INTERCEPTOR } from '@nestjs/core'; import { AppController } from './app.controller'; import { CacheInterceptor, CacheModule } from './cache'; -import { ConfigModule } from './config'; +import { ConfigModule, SERVER_FLAVOR } from './config'; import { EventModule } from './event'; import { BusinessModules } from './modules'; import { AuthModule } from './modules/auth'; @@ -29,6 +29,6 @@ const BasicModules = [ }, ], imports: [...BasicModules, ...BusinessModules], - controllers: [AppController], + controllers: SERVER_FLAVOR === 'selfhosted' ? [] : [AppController], }) export class AppModule {} diff --git a/packages/backend/server/src/config/def.ts b/packages/backend/server/src/config/def.ts index a3f9c2288f..34221ff3d9 100644 --- a/packages/backend/server/src/config/def.ts +++ b/packages/backend/server/src/config/def.ts @@ -91,6 +91,7 @@ export interface AFFiNEConfig { * @env NODE_ENV */ readonly env: string; + /** * fast AFFiNE environment judge */ diff --git a/packages/backend/server/src/config/default.ts b/packages/backend/server/src/config/default.ts index ec3233cfbf..eca97146f2 100644 --- a/packages/backend/server/src/config/default.ts +++ b/packages/backend/server/src/config/default.ts @@ -49,6 +49,7 @@ const jwtKeyPair = (function () { })(); export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => { + let isHttps: boolean | null = null; const defaultConfig = { serverId: 'affine-nestjs-server', version: pkg.version, @@ -56,6 +57,7 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => { AFFINE_SERVER_PORT: ['port', 'int'], AFFINE_SERVER_HOST: 'host', AFFINE_SERVER_SUB_PATH: 'path', + AFFIHE_SERVER_HTTPS: 'https', AFFINE_ENV: 'affineEnv', DATABASE_URL: 'db.url', ENABLE_CAPTCHA: ['auth.captcha.enable', 'boolean'], @@ -117,7 +119,10 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => { earlyAccessPreview: false, }, get https() { - return !this.node.dev; + return isHttps ?? !this.node.dev; + }, + set https(value: boolean) { + isHttps = value; }, host: 'localhost', port: 3010, diff --git a/packages/backend/server/src/metrics/opentelemetry.ts b/packages/backend/server/src/metrics/opentelemetry.ts index 38355a389d..54e449ae13 100644 --- a/packages/backend/server/src/metrics/opentelemetry.ts +++ b/packages/backend/server/src/metrics/opentelemetry.ts @@ -33,6 +33,7 @@ import prismaInstrument from '@prisma/instrumentation'; const { PrismaInstrumentation } = prismaInstrument; +import { parseEnvValue } from '../config/def'; import { PrismaMetricProducer } from './prisma'; abstract class OpentelemetryFactor { @@ -155,6 +156,9 @@ export function getMeter(name = 'business') { } export function start() { + if (parseEnvValue(process.env.DISABLE_TELEMETRY, 'boolean')) { + return; + } const sdk = createSDK(); if (sdk) { diff --git a/packages/backend/server/src/modules/auth/next-auth-options.ts b/packages/backend/server/src/modules/auth/next-auth-options.ts index fa40774a27..841f244249 100644 --- a/packages/backend/server/src/modules/auth/next-auth-options.ts +++ b/packages/backend/server/src/modules/auth/next-auth-options.ts @@ -179,7 +179,7 @@ export const NextAuthOptionsProvider: FactoryProvider = { ); } - if (config.auth.oauthProviders.google) { + if (config.auth.oauthProviders.google?.enabled) { nextAuthOptions.providers.push( // @ts-expect-error esm interop issue Google.default({ diff --git a/packages/backend/server/src/modules/auth/next-auth.controller.ts b/packages/backend/server/src/modules/auth/next-auth.controller.ts index 9d717f0121..fee533ecea 100644 --- a/packages/backend/server/src/modules/auth/next-auth.controller.ts +++ b/packages/backend/server/src/modules/auth/next-auth.controller.ts @@ -186,15 +186,17 @@ export class NextAuthController { } let nextAuthTokenCookie: (CookieOption & { value: string }) | undefined; - const cookiePrefix = this.config.node.prod ? '__Secure-' : ''; - const sessionCookieName = `${cookiePrefix}next-auth.session-token`; + const secureCookiePrefix = '__Secure-'; + const sessionCookieName = `next-auth.session-token`; // next-auth credentials login only support JWT strategy // https://next-auth.js.org/configuration/providers/credentials // let's store the session token in the database if ( credentialsSignIn && (nextAuthTokenCookie = cookies?.find( - ({ name }) => name === sessionCookieName + ({ name }) => + name === sessionCookieName || + name === `${secureCookiePrefix}${sessionCookieName}` )) ) { const cookieExpires = new Date(); diff --git a/packages/backend/server/src/modules/index.ts b/packages/backend/server/src/modules/index.ts index c2edffda85..0522f3e2dd 100644 --- a/packages/backend/server/src/modules/index.ts +++ b/packages/backend/server/src/modules/index.ts @@ -1,5 +1,8 @@ +import { join } from 'node:path'; + import { DynamicModule, Type } from '@nestjs/common'; import { ScheduleModule } from '@nestjs/schedule'; +import { ServeStaticModule } from '@nestjs/serve-static'; import { SERVER_FLAVOR } from '../config'; import { GqlModule } from '../graphql.module'; @@ -29,7 +32,10 @@ switch (SERVER_FLAVOR) { UsersModule, SyncModule, DocModule, - StorageModule + StorageModule, + ServeStaticModule.forRoot({ + rootPath: join('/app', 'static'), + }) ); break; case 'graphql': diff --git a/packages/backend/server/src/prelude.ts b/packages/backend/server/src/prelude.ts index bac73ad323..2ca5041763 100644 --- a/packages/backend/server/src/prelude.ts +++ b/packages/backend/server/src/prelude.ts @@ -1,5 +1,17 @@ +import { homedir } from 'node:os'; +import { join } from 'node:path'; + +import { config } from 'dotenv'; + +import { SERVER_FLAVOR } from './config/default'; + +if (SERVER_FLAVOR === 'selfhosted') { + config({ path: join(homedir(), '.affine', '.env') }); +} else { + config(); +} + import 'reflect-metadata'; -import 'dotenv/config'; import './affine'; import './affine.config'; diff --git a/packages/backend/server/src/storage/index.ts b/packages/backend/server/src/storage/index.ts index ad121b5ad8..8d2fc87b18 100644 --- a/packages/backend/server/src/storage/index.ts +++ b/packages/backend/server/src/storage/index.ts @@ -10,7 +10,9 @@ try { storageModule = process.arch === 'arm64' ? require('../../storage.arm64.node') - : require('../../storage.node'); + : process.arch === 'arm' + ? require('../../storage.armv7.node') + : require('../../storage.node'); } export { storageModule as OctoBaseStorageModule }; diff --git a/packages/frontend/core/.webpack/config.ts b/packages/frontend/core/.webpack/config.ts index 83a3b1b5b5..e9a92b2008 100644 --- a/packages/frontend/core/.webpack/config.ts +++ b/packages/frontend/core/.webpack/config.ts @@ -72,7 +72,10 @@ const OptimizeOptionOptions: ( export const getPublicPath = (buildFlags: BuildFlags) => { const { BUILD_TYPE } = process.env; - const publicPath = process.env.PUBLIC_PATH ?? '/'; + if (typeof process.env.PUBLIC_PATH === 'string') { + return process.env.PUBLIC_PATH; + } + const publicPath = '/'; if (process.env.COVERAGE || buildFlags.distribution === 'desktop') { return publicPath; } diff --git a/packages/frontend/workspace-impl/src/cloud/sync/index.ts b/packages/frontend/workspace-impl/src/cloud/sync/index.ts index 14f4588690..22225fcc3a 100644 --- a/packages/frontend/workspace-impl/src/cloud/sync/index.ts +++ b/packages/frontend/workspace-impl/src/cloud/sync/index.ts @@ -172,8 +172,7 @@ export function createAffineStaticStorage(workspaceId: string): SyncStorage { name: 'affine-cloud-static', async pull(docId) { const response = await fetchWithTraceReport( - runtimeConfig.serverUrlPrefix + - `/api/workspaces/${workspaceId}/docs/${docId}`, + `/api/workspaces/${workspaceId}/docs/${docId}`, { priority: 'high', } diff --git a/scripts/download-blocksuite-fonts.mjs b/scripts/download-blocksuite-fonts.mjs new file mode 100644 index 0000000000..38d5115f72 --- /dev/null +++ b/scripts/download-blocksuite-fonts.mjs @@ -0,0 +1,29 @@ +import { writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +import { DEFAULT_CANVAS_TEXT_FONT_CONFIG } from '@blocksuite/blocks/dist/surface-block/consts.js'; + +const fontPath = join( + fileURLToPath(import.meta.url), + '..', + '..', + 'packages', + 'frontend', + 'core', + 'dist', + 'assets' +); + +await Promise.all( + DEFAULT_CANVAS_TEXT_FONT_CONFIG.map(async ({ url }) => { + const buffer = await fetch(url).then(res => + res.arrayBuffer().then(res => Buffer.from(res)) + ); + const filename = url.split('/').pop(); + const distPath = join(fontPath, filename); + await writeFile(distPath, buffer); + console.info(`Downloaded ${distPath} successfully`); + }) +); diff --git a/yarn.lock b/yarn.lock index d4ba009b01..5ee1c3df18 100644 --- a/yarn.lock +++ b/yarn.lock @@ -637,6 +637,7 @@ __metadata: "@nestjs/platform-express": "npm:^10.2.10" "@nestjs/platform-socket.io": "npm:^10.2.10" "@nestjs/schedule": "npm:^4.0.0" + "@nestjs/serve-static": "npm:^4.0.0" "@nestjs/testing": "npm:^10.2.10" "@nestjs/throttler": "npm:^5.0.1" "@nestjs/websockets": "npm:^10.2.10" @@ -678,6 +679,7 @@ __metadata: c8: "npm:^9.0.0" cookie-parser: "npm:^1.4.6" dotenv: "npm:^16.3.1" + dotenv-cli: "npm:^7.3.0" express: "npm:^4.18.2" file-type: "npm:^19.0.0" get-stream: "npm:^8.0.1" @@ -7445,6 +7447,28 @@ __metadata: languageName: node linkType: hard +"@nestjs/serve-static@npm:^4.0.0": + version: 4.0.0 + resolution: "@nestjs/serve-static@npm:4.0.0" + dependencies: + path-to-regexp: "npm:0.2.5" + peerDependencies: + "@fastify/static": ^6.5.0 + "@nestjs/common": ^9.0.0 || ^10.0.0 + "@nestjs/core": ^9.0.0 || ^10.0.0 + express: ^4.18.1 + fastify: ^4.7.0 + peerDependenciesMeta: + "@fastify/static": + optional: true + express: + optional: true + fastify: + optional: true + checksum: f9dde33701d05fe0309ecc4912c00b6ed81945dbeb17af13562b33406656ae6574b14397523e21c90b78292b073a510426409c08e5f6a2b88b46d73dc51faa9c + languageName: node + linkType: hard + "@nestjs/testing@npm:^10.2.10": version: 10.2.10 resolution: "@nestjs/testing@npm:10.2.10" @@ -18794,6 +18818,20 @@ __metadata: languageName: node linkType: hard +"dotenv-cli@npm:^7.3.0": + version: 7.3.0 + resolution: "dotenv-cli@npm:7.3.0" + dependencies: + cross-spawn: "npm:^7.0.3" + dotenv: "npm:^16.3.0" + dotenv-expand: "npm:^10.0.0" + minimist: "npm:^1.2.6" + bin: + dotenv: cli.js + checksum: bc48e9872ed451aa7633cfde0079f5e4b40837d49dca4eab947682c80f524bd1e63ec31ff69b7cf955ff969185a05a343dd5d754dd5569e4ae31f8e9a790ab1b + languageName: node + linkType: hard + "dotenv-expand@npm:^10.0.0, dotenv-expand@npm:~10.0.0": version: 10.0.0 resolution: "dotenv-expand@npm:10.0.0" @@ -18801,7 +18839,7 @@ __metadata: languageName: node linkType: hard -"dotenv@npm:^16.0.0, dotenv@npm:^16.3.1, dotenv@npm:~16.3.1": +"dotenv@npm:^16.0.0, dotenv@npm:^16.3.0, dotenv@npm:^16.3.1, dotenv@npm:~16.3.1": version: 16.3.1 resolution: "dotenv@npm:16.3.1" checksum: dbb778237ef8750e9e3cd1473d3c8eaa9cc3600e33a75c0e36415d0fa0848197f56c3800f77924c70e7828f0b03896818cd52f785b07b9ad4d88dba73fbba83f @@ -28540,6 +28578,13 @@ __metadata: languageName: node linkType: hard +"path-to-regexp@npm:0.2.5": + version: 0.2.5 + resolution: "path-to-regexp@npm:0.2.5" + checksum: 9652fd2b74ec932a0df8a868478478565da81e7445a8dde1e65ca80553ad03062336b1f79234068551ecc01f3b76ad774e34f784cc3a34a97c4646cb5cfcbea9 + languageName: node + linkType: hard + "path-to-regexp@npm:2.2.1": version: 2.2.1 resolution: "path-to-regexp@npm:2.2.1" From 5aee480c50e35d598fbe2b4ad861987431961791 Mon Sep 17 00:00:00 2001 From: EYHN Date: Wed, 10 Jan 2024 11:09:39 +0000 Subject: [PATCH 05/92] refactor(core): move page list to core (#5556) --- .../app-sidebar/sidebar-containers/index.tsx | 2 ++ .../components/date-picker/{index.ts => index.tsx} | 0 packages/frontend/core/package.json | 5 +++++ packages/frontend/core/src/atoms/collections.ts | 8 ++++---- .../components/affine/auth/subscription-redirect.tsx | 2 +- .../share-page-modal/share-menu/share-export.tsx | 2 +- .../block-suite-header-title/operation-menu.tsx | 6 +++++- .../components/page-list/__tests__/filter.spec.tsx | 0 .../page-list/__tests__/use-all-page-setting.spec.ts | 0 .../__tests__/use-block-suite-page-preview.spec.ts | 0 .../components/page-list/components/favorite-tag.tsx | 4 ++-- .../components/favorited-animation/data.json | 0 .../page-list/components/floating-toobar.tsx | 0 .../page-list/components/floating-toolbar.css.ts | 0 .../page-list/components/new-page-button.css.ts | 0 .../page-list/components/new-page-buttton.tsx | 5 ++--- .../src/components/page-list/filter/condition.tsx | 2 +- .../src/components/page-list/filter/eval.ts | 0 .../src/components/page-list/filter/filter-list.tsx | 4 +--- .../page-list/filter/filter-tag-translation.tsx | 0 .../src/components/page-list/filter/index.css.ts | 0 .../src/components/page-list/filter/index.ts | 0 .../components/page-list/filter/literal-matcher.tsx | 5 ++--- .../page-list/filter/logical/custom-type.ts | 0 .../components/page-list/filter/logical/matcher.ts | 0 .../page-list/filter/logical/typesystem.ts | 0 .../components/page-list/filter/multi-select.css.ts | 0 .../src/components/page-list/filter/multi-select.tsx | 2 +- .../src/components/page-list/filter/shared-types.tsx | 0 .../src/components/page-list/filter/utils.ts | 0 .../src/components/page-list/filter/vars.tsx | 2 +- .../src/components/page-list/index.tsx | 0 .../src/components/page-list/operation-cell.tsx | 12 ++++++++---- .../operation-menu-items/disable-public-sharing.tsx | 5 ++--- .../page-list/operation-menu-items/export.tsx | 2 +- .../operation-menu-items/get-content-parser.ts | 0 .../page-list/operation-menu-items/index.css.ts | 0 .../page-list/operation-menu-items/index.ts | 0 .../page-list/operation-menu-items/move-to-trash.tsx | 10 +++++++--- .../page-list/operation-menu-items/types.ts | 0 .../components/page-list/page-content-preview.tsx | 0 .../src/components/page-list/page-group.css.ts | 0 .../src/components/page-list/page-group.tsx | 0 .../src/components/page-list/page-header.tsx | 2 +- .../src/components/page-list/page-list-item.css.ts | 0 .../src/components/page-list/page-list-item.tsx | 2 +- .../src/components/page-list/page-list.css.ts | 0 .../src/components/page-list/page-list.tsx | 4 ++-- .../src/components/page-list/page-tags.css.ts | 0 .../src/components/page-list/page-tags.tsx | 2 +- .../src/components/page-list/pages-to-page-group.tsx | 0 .../src/components/page-list/readme.md | 0 .../src/components/page-list/scoped-atoms.tsx | 0 .../src/components/page-list/types.ts | 0 .../page-list/use-block-suite-page-preview.ts | 0 .../page-list/use-block-suite-workspace-page.ts | 0 .../components/page-list/use-collection-manager.ts | 0 .../page-list/use-page-selection-events.ts | 0 .../src/components/page-list/utils.tsx | 0 .../src/components/page-list/view/affine-shape.tsx | 0 .../components/page-list/view/collection-bar.css.ts | 0 .../src/components/page-list/view/collection-bar.tsx | 3 +-- .../components/page-list/view/collection-list.css.ts | 0 .../components/page-list/view/collection-list.tsx | 6 +++--- .../page-list/view/collection-operations.css.ts | 0 .../page-list/view/collection-operations.tsx | 7 ++++++- .../page-list/view/create-collection.css.ts | 0 .../components/page-list/view/create-collection.tsx | 4 +--- .../view/edit-collection/edit-collection.css.ts | 0 .../view/edit-collection/edit-collection.tsx | 9 ++++++--- .../page-list/view/edit-collection/hooks.tsx | 2 +- .../page-list/view/edit-collection/pages-mode.tsx | 2 +- .../page-list/view/edit-collection/rules-mode.tsx | 0 .../page-list/view/edit-collection/select-page.tsx | 3 +-- .../page-list/view/edit-collection/use-filter.tsx | 0 .../page-list/view/edit-collection/use-search.tsx | 0 .../src/components/page-list/view/index.ts | 0 .../page-list/view/save-as-collection-button.tsx | 2 +- .../src/components/page-list/view/use-action.tsx | 0 .../page-list/view/use-edit-collection.tsx | 0 .../components/page-list/virtualized-page-list.tsx | 2 +- .../frontend/core/src/components/pure/cmdk/data.tsx | 2 +- .../frontend/core/src/components/pure/cmdk/main.tsx | 2 +- .../collections/collections-list.tsx | 6 +++--- .../core/src/components/root-app-sidebar/index.tsx | 12 ++++++------ .../src/components/workspace-upgrade/upgrade.tsx | 2 +- .../src/hooks/affine/use-all-page-list-config.tsx | 2 +- .../core/src/hooks/affine/use-sidebar-drag.ts | 2 +- .../frontend/core/src/layouts/workspace-layout.tsx | 8 ++++---- .../src/pages/workspace/all-page/all-page-filter.tsx | 10 +++++----- .../core/src/pages/workspace/all-page/all-page.tsx | 2 +- .../frontend/core/src/pages/workspace/collection.tsx | 2 +- .../src/pages/workspace/detail-page/detail-page.tsx | 8 ++++---- packages/frontend/core/src/pages/workspace/pages.tsx | 2 +- .../frontend/core/src/pages/workspace/trash-page.tsx | 2 +- tests/storybook/project.json | 1 + tests/storybook/src/stories/page-list.stories.tsx | 2 +- yarn.lock | 5 +++++ 98 files changed, 111 insertions(+), 87 deletions(-) rename packages/frontend/component/src/components/date-picker/{index.ts => index.tsx} (100%) rename packages/frontend/{component => core}/src/components/page-list/__tests__/filter.spec.tsx (100%) rename packages/frontend/{component => core}/src/components/page-list/__tests__/use-all-page-setting.spec.ts (100%) rename packages/frontend/{component => core}/src/components/page-list/__tests__/use-block-suite-page-preview.spec.ts (100%) rename packages/frontend/{component => core}/src/components/page-list/components/favorite-tag.tsx (92%) rename packages/frontend/{component => core}/src/components/page-list/components/favorited-animation/data.json (100%) rename packages/frontend/{component => core}/src/components/page-list/components/floating-toobar.tsx (100%) rename packages/frontend/{component => core}/src/components/page-list/components/floating-toolbar.css.ts (100%) rename packages/frontend/{component => core}/src/components/page-list/components/new-page-button.css.ts (100%) rename packages/frontend/{component => core}/src/components/page-list/components/new-page-buttton.tsx (95%) rename packages/frontend/{component => core}/src/components/page-list/filter/condition.tsx (98%) rename packages/frontend/{component => core}/src/components/page-list/filter/eval.ts (100%) rename packages/frontend/{component => core}/src/components/page-list/filter/filter-list.tsx (93%) rename packages/frontend/{component => core}/src/components/page-list/filter/filter-tag-translation.tsx (100%) rename packages/frontend/{component => core}/src/components/page-list/filter/index.css.ts (100%) rename packages/frontend/{component => core}/src/components/page-list/filter/index.ts (100%) rename packages/frontend/{component => core}/src/components/page-list/filter/literal-matcher.tsx (94%) rename packages/frontend/{component => core}/src/components/page-list/filter/logical/custom-type.ts (100%) rename packages/frontend/{component => core}/src/components/page-list/filter/logical/matcher.ts (100%) rename packages/frontend/{component => core}/src/components/page-list/filter/logical/typesystem.ts (100%) rename packages/frontend/{component => core}/src/components/page-list/filter/multi-select.css.ts (100%) rename packages/frontend/{component => core}/src/components/page-list/filter/multi-select.tsx (96%) rename packages/frontend/{component => core}/src/components/page-list/filter/shared-types.tsx (100%) rename packages/frontend/{component => core}/src/components/page-list/filter/utils.ts (100%) rename packages/frontend/{component => core}/src/components/page-list/filter/vars.tsx (99%) rename packages/frontend/{component => core}/src/components/page-list/index.tsx (100%) rename packages/frontend/{component => core}/src/components/page-list/operation-cell.tsx (96%) rename packages/frontend/{component => core}/src/components/page-list/operation-menu-items/disable-public-sharing.tsx (75%) rename packages/frontend/{component => core}/src/components/page-list/operation-menu-items/export.tsx (97%) rename packages/frontend/{component => core}/src/components/page-list/operation-menu-items/get-content-parser.ts (100%) rename packages/frontend/{component => core}/src/components/page-list/operation-menu-items/index.css.ts (100%) rename packages/frontend/{component => core}/src/components/page-list/operation-menu-items/index.ts (100%) rename packages/frontend/{component => core}/src/components/page-list/operation-menu-items/move-to-trash.tsx (90%) rename packages/frontend/{component => core}/src/components/page-list/operation-menu-items/types.ts (100%) rename packages/frontend/{component => core}/src/components/page-list/page-content-preview.tsx (100%) rename packages/frontend/{component => core}/src/components/page-list/page-group.css.ts (100%) rename packages/frontend/{component => core}/src/components/page-list/page-group.tsx (100%) rename packages/frontend/{component => core}/src/components/page-list/page-header.tsx (98%) rename packages/frontend/{component => core}/src/components/page-list/page-list-item.css.ts (100%) rename packages/frontend/{component => core}/src/components/page-list/page-list-item.tsx (99%) rename packages/frontend/{component => core}/src/components/page-list/page-list.css.ts (100%) rename packages/frontend/{component => core}/src/components/page-list/page-list.tsx (97%) rename packages/frontend/{component => core}/src/components/page-list/page-tags.css.ts (100%) rename packages/frontend/{component => core}/src/components/page-list/page-tags.tsx (98%) rename packages/frontend/{component => core}/src/components/page-list/pages-to-page-group.tsx (100%) rename packages/frontend/{component => core}/src/components/page-list/readme.md (100%) rename packages/frontend/{component => core}/src/components/page-list/scoped-atoms.tsx (100%) rename packages/frontend/{component => core}/src/components/page-list/types.ts (100%) rename packages/frontend/{component => core}/src/components/page-list/use-block-suite-page-preview.ts (100%) rename packages/frontend/{component => core}/src/components/page-list/use-block-suite-workspace-page.ts (100%) rename packages/frontend/{component => core}/src/components/page-list/use-collection-manager.ts (100%) rename packages/frontend/{component => core}/src/components/page-list/use-page-selection-events.ts (100%) rename packages/frontend/{component => core}/src/components/page-list/utils.tsx (100%) rename packages/frontend/{component => core}/src/components/page-list/view/affine-shape.tsx (100%) rename packages/frontend/{component => core}/src/components/page-list/view/collection-bar.css.ts (100%) rename packages/frontend/{component => core}/src/components/page-list/view/collection-bar.tsx (97%) rename packages/frontend/{component => core}/src/components/page-list/view/collection-list.css.ts (100%) rename packages/frontend/{component => core}/src/components/page-list/view/collection-list.tsx (95%) rename packages/frontend/{component => core}/src/components/page-list/view/collection-operations.css.ts (100%) rename packages/frontend/{component => core}/src/components/page-list/view/collection-operations.tsx (97%) rename packages/frontend/{component => core}/src/components/page-list/view/create-collection.css.ts (100%) rename packages/frontend/{component => core}/src/components/page-list/view/create-collection.tsx (95%) rename packages/frontend/{component => core}/src/components/page-list/view/edit-collection/edit-collection.css.ts (100%) rename packages/frontend/{component => core}/src/components/page-list/view/edit-collection/edit-collection.tsx (97%) rename packages/frontend/{component => core}/src/components/page-list/view/edit-collection/hooks.tsx (96%) rename packages/frontend/{component => core}/src/components/page-list/view/edit-collection/pages-mode.tsx (99%) rename packages/frontend/{component => core}/src/components/page-list/view/edit-collection/rules-mode.tsx (100%) rename packages/frontend/{component => core}/src/components/page-list/view/edit-collection/select-page.tsx (98%) rename packages/frontend/{component => core}/src/components/page-list/view/edit-collection/use-filter.tsx (100%) rename packages/frontend/{component => core}/src/components/page-list/view/edit-collection/use-search.tsx (100%) rename packages/frontend/{component => core}/src/components/page-list/view/index.ts (100%) rename packages/frontend/{component => core}/src/components/page-list/view/save-as-collection-button.tsx (96%) rename packages/frontend/{component => core}/src/components/page-list/view/use-action.tsx (100%) rename packages/frontend/{component => core}/src/components/page-list/view/use-edit-collection.tsx (100%) rename packages/frontend/{component => core}/src/components/page-list/virtualized-page-list.tsx (99%) diff --git a/packages/frontend/component/src/components/app-sidebar/sidebar-containers/index.tsx b/packages/frontend/component/src/components/app-sidebar/sidebar-containers/index.tsx index b67199bb45..1da9fb59d1 100644 --- a/packages/frontend/component/src/components/app-sidebar/sidebar-containers/index.tsx +++ b/packages/frontend/component/src/components/app-sidebar/sidebar-containers/index.tsx @@ -5,6 +5,8 @@ import { type PropsWithChildren, useRef } from 'react'; import * as styles from './index.css'; import { useHasScrollTop } from './use-has-scroll-top'; +export { useHasScrollTop } from './use-has-scroll-top'; + export function SidebarContainer({ children }: PropsWithChildren) { return
{children}
; } diff --git a/packages/frontend/component/src/components/date-picker/index.ts b/packages/frontend/component/src/components/date-picker/index.tsx similarity index 100% rename from packages/frontend/component/src/components/date-picker/index.ts rename to packages/frontend/component/src/components/date-picker/index.tsx diff --git a/packages/frontend/core/package.json b/packages/frontend/core/package.json index 77e22c8d82..5f9e800f70 100644 --- a/packages/frontend/core/package.json +++ b/packages/frontend/core/package.json @@ -43,8 +43,10 @@ "@marsidev/react-turnstile": "^0.4.0", "@radix-ui/react-collapsible": "^1.0.3", "@radix-ui/react-dialog": "^1.0.4", + "@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-scroll-area": "^1.0.5", "@radix-ui/react-select": "^2.0.0", + "@radix-ui/react-toolbar": "^1.0.4", "@react-hookz/web": "^24.0.0", "@sentry/integrations": "^7.83.0", "@sentry/react": "^7.83.0", @@ -66,8 +68,10 @@ "jotai": "^2.5.1", "jotai-devtools": "^0.7.0", "jotai-effect": "^0.2.3", + "jotai-scope": "^0.4.1", "lit": "^3.0.2", "lodash-es": "^4.17.21", + "lottie-react": "^2.4.0", "lottie-web": "^5.12.2", "mini-css-extract-plugin": "^2.7.6", "nanoid": "^5.0.3", @@ -79,6 +83,7 @@ "react-error-boundary": "^4.0.11", "react-is": "18.2.0", "react-router-dom": "^6.16.0", + "react-virtuoso": "^4.6.2", "rxjs": "^7.8.1", "ses": "^1.0.0", "swr": "2.2.4", diff --git a/packages/frontend/core/src/atoms/collections.ts b/packages/frontend/core/src/atoms/collections.ts index 75bbddf0ea..098bddc658 100644 --- a/packages/frontend/core/src/atoms/collections.ts +++ b/packages/frontend/core/src/atoms/collections.ts @@ -1,7 +1,3 @@ -import type { - CollectionsCRUD, - CollectionsCRUDAtom, -} from '@affine/component/page-list'; import { currentWorkspaceAtom, waitForCurrentWorkspaceAtom, @@ -14,6 +10,10 @@ import { atom } from 'jotai'; import { atomWithObservable } from 'jotai/utils'; import { Observable, of } from 'rxjs'; +import type { + CollectionsCRUD, + CollectionsCRUDAtom, +} from '../components/page-list'; import { getUserSetting } from '../utils/user-setting'; import { getWorkspaceSetting } from '../utils/workspace-setting'; import { sessionAtom } from './cloud-user'; diff --git a/packages/frontend/core/src/components/affine/auth/subscription-redirect.tsx b/packages/frontend/core/src/components/affine/auth/subscription-redirect.tsx index 36b225455f..908f8eb0b0 100644 --- a/packages/frontend/core/src/components/affine/auth/subscription-redirect.tsx +++ b/packages/frontend/core/src/components/affine/auth/subscription-redirect.tsx @@ -1,7 +1,7 @@ import { SignUpPage } from '@affine/component/auth-components'; -import { AffineShapeIcon } from '@affine/component/page-list'; import { Button } from '@affine/component/ui/button'; import { Loading } from '@affine/component/ui/loading'; +import { AffineShapeIcon } from '@affine/core/components/page-list'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import type { SubscriptionRecurring } from '@affine/graphql'; import { diff --git a/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-export.tsx b/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-export.tsx index f30b059819..84376ba644 100644 --- a/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-export.tsx +++ b/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-export.tsx @@ -1,6 +1,6 @@ -import { ExportMenuItems } from '@affine/component/page-list'; import { Button } from '@affine/component/ui/button'; import { Divider } from '@affine/component/ui/divider'; +import { ExportMenuItems } from '@affine/core/components/page-list'; import { WorkspaceFlavour } from '@affine/env/workspace'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { LinkIcon } from '@blocksuite/icons'; diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-header-title/operation-menu.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-header-title/operation-menu.tsx index dac01089c6..459570096c 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-header-title/operation-menu.tsx +++ b/packages/frontend/core/src/components/blocksuite/block-suite-header-title/operation-menu.tsx @@ -1,10 +1,14 @@ -import { Export, FavoriteTag, MoveToTrash } from '@affine/component/page-list'; import { Menu, MenuIcon, MenuItem, MenuSeparator, } from '@affine/component/ui/menu'; +import { + Export, + FavoriteTag, + MoveToTrash, +} from '@affine/core/components/page-list'; import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta'; import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace'; diff --git a/packages/frontend/component/src/components/page-list/__tests__/filter.spec.tsx b/packages/frontend/core/src/components/page-list/__tests__/filter.spec.tsx similarity index 100% rename from packages/frontend/component/src/components/page-list/__tests__/filter.spec.tsx rename to packages/frontend/core/src/components/page-list/__tests__/filter.spec.tsx diff --git a/packages/frontend/component/src/components/page-list/__tests__/use-all-page-setting.spec.ts b/packages/frontend/core/src/components/page-list/__tests__/use-all-page-setting.spec.ts similarity index 100% rename from packages/frontend/component/src/components/page-list/__tests__/use-all-page-setting.spec.ts rename to packages/frontend/core/src/components/page-list/__tests__/use-all-page-setting.spec.ts diff --git a/packages/frontend/component/src/components/page-list/__tests__/use-block-suite-page-preview.spec.ts b/packages/frontend/core/src/components/page-list/__tests__/use-block-suite-page-preview.spec.ts similarity index 100% rename from packages/frontend/component/src/components/page-list/__tests__/use-block-suite-page-preview.spec.ts rename to packages/frontend/core/src/components/page-list/__tests__/use-block-suite-page-preview.spec.ts diff --git a/packages/frontend/component/src/components/page-list/components/favorite-tag.tsx b/packages/frontend/core/src/components/page-list/components/favorite-tag.tsx similarity index 92% rename from packages/frontend/component/src/components/page-list/components/favorite-tag.tsx rename to packages/frontend/core/src/components/page-list/components/favorite-tag.tsx index b035f68113..4e05f43260 100644 --- a/packages/frontend/component/src/components/page-list/components/favorite-tag.tsx +++ b/packages/frontend/core/src/components/page-list/components/favorite-tag.tsx @@ -1,10 +1,10 @@ +import { IconButton, type IconButtonProps } from '@affine/component'; +import { Tooltip } from '@affine/component'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { FavoritedIcon, FavoriteIcon } from '@blocksuite/icons'; import Lottie from 'lottie-react'; import { forwardRef, useCallback, useState } from 'react'; -import { IconButton, type IconButtonProps } from '../../../ui/button'; -import { Tooltip } from '../../../ui/tooltip'; import favoritedAnimation from './favorited-animation/data.json'; export const FavoriteTag = forwardRef< diff --git a/packages/frontend/component/src/components/page-list/components/favorited-animation/data.json b/packages/frontend/core/src/components/page-list/components/favorited-animation/data.json similarity index 100% rename from packages/frontend/component/src/components/page-list/components/favorited-animation/data.json rename to packages/frontend/core/src/components/page-list/components/favorited-animation/data.json diff --git a/packages/frontend/component/src/components/page-list/components/floating-toobar.tsx b/packages/frontend/core/src/components/page-list/components/floating-toobar.tsx similarity index 100% rename from packages/frontend/component/src/components/page-list/components/floating-toobar.tsx rename to packages/frontend/core/src/components/page-list/components/floating-toobar.tsx diff --git a/packages/frontend/component/src/components/page-list/components/floating-toolbar.css.ts b/packages/frontend/core/src/components/page-list/components/floating-toolbar.css.ts similarity index 100% rename from packages/frontend/component/src/components/page-list/components/floating-toolbar.css.ts rename to packages/frontend/core/src/components/page-list/components/floating-toolbar.css.ts diff --git a/packages/frontend/component/src/components/page-list/components/new-page-button.css.ts b/packages/frontend/core/src/components/page-list/components/new-page-button.css.ts similarity index 100% rename from packages/frontend/component/src/components/page-list/components/new-page-button.css.ts rename to packages/frontend/core/src/components/page-list/components/new-page-button.css.ts diff --git a/packages/frontend/component/src/components/page-list/components/new-page-buttton.tsx b/packages/frontend/core/src/components/page-list/components/new-page-buttton.tsx similarity index 95% rename from packages/frontend/component/src/components/page-list/components/new-page-buttton.tsx rename to packages/frontend/core/src/components/page-list/components/new-page-buttton.tsx index 8cdc0bb2ad..dfcd93e99f 100644 --- a/packages/frontend/component/src/components/page-list/components/new-page-buttton.tsx +++ b/packages/frontend/core/src/components/page-list/components/new-page-buttton.tsx @@ -1,10 +1,9 @@ +import { DropdownButton, Menu } from '@affine/component'; +import { BlockCard } from '@affine/component/card/block-card'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { EdgelessIcon, ImportIcon, PageIcon } from '@blocksuite/icons'; import { type PropsWithChildren, useCallback, useState } from 'react'; -import { DropdownButton } from '../../../ui/button'; -import { Menu } from '../../../ui/menu'; -import { BlockCard } from '../../card/block-card'; import { menuContent } from './new-page-button.css'; type NewPageButtonProps = { diff --git a/packages/frontend/component/src/components/page-list/filter/condition.tsx b/packages/frontend/core/src/components/page-list/filter/condition.tsx similarity index 98% rename from packages/frontend/component/src/components/page-list/filter/condition.tsx rename to packages/frontend/core/src/components/page-list/filter/condition.tsx index 76db433231..a2609643b3 100644 --- a/packages/frontend/component/src/components/page-list/filter/condition.tsx +++ b/packages/frontend/core/src/components/page-list/filter/condition.tsx @@ -1,9 +1,9 @@ +import { Menu, MenuItem } from '@affine/component'; import type { Filter, Literal } from '@affine/env/filter'; import type { PropertiesMeta } from '@affine/env/filter'; import type { ReactNode } from 'react'; import { useMemo } from 'react'; -import { Menu, MenuItem } from '../../../ui/menu'; import { FilterTag } from './filter-tag-translation'; import * as styles from './index.css'; import { literalMatcher } from './literal-matcher'; diff --git a/packages/frontend/component/src/components/page-list/filter/eval.ts b/packages/frontend/core/src/components/page-list/filter/eval.ts similarity index 100% rename from packages/frontend/component/src/components/page-list/filter/eval.ts rename to packages/frontend/core/src/components/page-list/filter/eval.ts diff --git a/packages/frontend/component/src/components/page-list/filter/filter-list.tsx b/packages/frontend/core/src/components/page-list/filter/filter-list.tsx similarity index 93% rename from packages/frontend/component/src/components/page-list/filter/filter-list.tsx rename to packages/frontend/core/src/components/page-list/filter/filter-list.tsx index 56d8a00783..7809a8cdd1 100644 --- a/packages/frontend/component/src/components/page-list/filter/filter-list.tsx +++ b/packages/frontend/core/src/components/page-list/filter/filter-list.tsx @@ -1,11 +1,9 @@ +import { Button, IconButton, Menu } from '@affine/component'; import type { Filter } from '@affine/env/filter'; import type { PropertiesMeta } from '@affine/env/filter'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { CloseIcon, PlusIcon } from '@blocksuite/icons'; -import { Button } from '../../../ui/button'; -import { IconButton } from '../../../ui/button'; -import { Menu } from '../../../ui/menu'; import { Condition } from './condition'; import * as styles from './index.css'; import { CreateFilterMenu } from './vars'; diff --git a/packages/frontend/component/src/components/page-list/filter/filter-tag-translation.tsx b/packages/frontend/core/src/components/page-list/filter/filter-tag-translation.tsx similarity index 100% rename from packages/frontend/component/src/components/page-list/filter/filter-tag-translation.tsx rename to packages/frontend/core/src/components/page-list/filter/filter-tag-translation.tsx diff --git a/packages/frontend/component/src/components/page-list/filter/index.css.ts b/packages/frontend/core/src/components/page-list/filter/index.css.ts similarity index 100% rename from packages/frontend/component/src/components/page-list/filter/index.css.ts rename to packages/frontend/core/src/components/page-list/filter/index.css.ts diff --git a/packages/frontend/component/src/components/page-list/filter/index.ts b/packages/frontend/core/src/components/page-list/filter/index.ts similarity index 100% rename from packages/frontend/component/src/components/page-list/filter/index.ts rename to packages/frontend/core/src/components/page-list/filter/index.ts diff --git a/packages/frontend/component/src/components/page-list/filter/literal-matcher.tsx b/packages/frontend/core/src/components/page-list/filter/literal-matcher.tsx similarity index 94% rename from packages/frontend/component/src/components/page-list/filter/literal-matcher.tsx rename to packages/frontend/core/src/components/page-list/filter/literal-matcher.tsx index 6a4a77e1dc..83cacf6f8d 100644 --- a/packages/frontend/component/src/components/page-list/filter/literal-matcher.tsx +++ b/packages/frontend/core/src/components/page-list/filter/literal-matcher.tsx @@ -1,10 +1,9 @@ +import { Input, Menu, MenuItem } from '@affine/component'; +import { AFFiNEDatePicker } from '@affine/component/date-picker'; import type { LiteralValue, Tag } from '@affine/env/filter'; import dayjs from 'dayjs'; import { type ReactNode } from 'react'; -import Input from '../../../ui/input'; -import { Menu, MenuItem } from '../../../ui/menu'; -import { AFFiNEDatePicker } from '../../date-picker'; import { FilterTag } from './filter-tag-translation'; import { inputStyle } from './index.css'; import { tBoolean, tDate, tDateRange, tTag } from './logical/custom-type'; diff --git a/packages/frontend/component/src/components/page-list/filter/logical/custom-type.ts b/packages/frontend/core/src/components/page-list/filter/logical/custom-type.ts similarity index 100% rename from packages/frontend/component/src/components/page-list/filter/logical/custom-type.ts rename to packages/frontend/core/src/components/page-list/filter/logical/custom-type.ts diff --git a/packages/frontend/component/src/components/page-list/filter/logical/matcher.ts b/packages/frontend/core/src/components/page-list/filter/logical/matcher.ts similarity index 100% rename from packages/frontend/component/src/components/page-list/filter/logical/matcher.ts rename to packages/frontend/core/src/components/page-list/filter/logical/matcher.ts diff --git a/packages/frontend/component/src/components/page-list/filter/logical/typesystem.ts b/packages/frontend/core/src/components/page-list/filter/logical/typesystem.ts similarity index 100% rename from packages/frontend/component/src/components/page-list/filter/logical/typesystem.ts rename to packages/frontend/core/src/components/page-list/filter/logical/typesystem.ts diff --git a/packages/frontend/component/src/components/page-list/filter/multi-select.css.ts b/packages/frontend/core/src/components/page-list/filter/multi-select.css.ts similarity index 100% rename from packages/frontend/component/src/components/page-list/filter/multi-select.css.ts rename to packages/frontend/core/src/components/page-list/filter/multi-select.css.ts diff --git a/packages/frontend/component/src/components/page-list/filter/multi-select.tsx b/packages/frontend/core/src/components/page-list/filter/multi-select.tsx similarity index 96% rename from packages/frontend/component/src/components/page-list/filter/multi-select.tsx rename to packages/frontend/core/src/components/page-list/filter/multi-select.tsx index 84264c6ff6..41dd71552a 100644 --- a/packages/frontend/component/src/components/page-list/filter/multi-select.tsx +++ b/packages/frontend/core/src/components/page-list/filter/multi-select.tsx @@ -1,7 +1,7 @@ +import { Menu, MenuItem } from '@affine/component'; import type { MouseEvent } from 'react'; import { useMemo } from 'react'; -import { Menu, MenuItem } from '../../../ui/menu'; import * as styles from './multi-select.css'; export const MultiSelect = ({ diff --git a/packages/frontend/component/src/components/page-list/filter/shared-types.tsx b/packages/frontend/core/src/components/page-list/filter/shared-types.tsx similarity index 100% rename from packages/frontend/component/src/components/page-list/filter/shared-types.tsx rename to packages/frontend/core/src/components/page-list/filter/shared-types.tsx diff --git a/packages/frontend/component/src/components/page-list/filter/utils.ts b/packages/frontend/core/src/components/page-list/filter/utils.ts similarity index 100% rename from packages/frontend/component/src/components/page-list/filter/utils.ts rename to packages/frontend/core/src/components/page-list/filter/utils.ts diff --git a/packages/frontend/component/src/components/page-list/filter/vars.tsx b/packages/frontend/core/src/components/page-list/filter/vars.tsx similarity index 99% rename from packages/frontend/component/src/components/page-list/filter/vars.tsx rename to packages/frontend/core/src/components/page-list/filter/vars.tsx index a1b36cf8b0..71a941ca16 100644 --- a/packages/frontend/component/src/components/page-list/filter/vars.tsx +++ b/packages/frontend/core/src/components/page-list/filter/vars.tsx @@ -1,3 +1,4 @@ +import { MenuIcon, MenuItem, MenuSeparator } from '@affine/component'; import type { Filter, LiteralValue, @@ -8,7 +9,6 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks'; import dayjs from 'dayjs'; import type { ReactNode } from 'react'; -import { MenuIcon, MenuItem, MenuSeparator } from '../../../ui/menu'; import { FilterTag } from './filter-tag-translation'; import * as styles from './index.css'; import { tBoolean, tDate, tDateRange, tTag } from './logical/custom-type'; diff --git a/packages/frontend/component/src/components/page-list/index.tsx b/packages/frontend/core/src/components/page-list/index.tsx similarity index 100% rename from packages/frontend/component/src/components/page-list/index.tsx rename to packages/frontend/core/src/components/page-list/index.tsx diff --git a/packages/frontend/component/src/components/page-list/operation-cell.tsx b/packages/frontend/core/src/components/page-list/operation-cell.tsx similarity index 96% rename from packages/frontend/component/src/components/page-list/operation-cell.tsx rename to packages/frontend/core/src/components/page-list/operation-cell.tsx index a8d8d8d08d..51aef06492 100644 --- a/packages/frontend/component/src/components/page-list/operation-cell.tsx +++ b/packages/frontend/core/src/components/page-list/operation-cell.tsx @@ -1,3 +1,11 @@ +import { + ConfirmModal, + IconButton, + Menu, + MenuIcon, + MenuItem, + Tooltip, +} from '@affine/component'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { DeletePermanentlyIcon, @@ -10,10 +18,6 @@ import { import { useState } from 'react'; import { Link } from 'react-router-dom'; -import { IconButton } from '../../ui/button'; -import { Menu, MenuIcon, MenuItem } from '../../ui/menu'; -import { ConfirmModal } from '../../ui/modal'; -import { Tooltip } from '../../ui/tooltip'; import { FavoriteTag } from './components/favorite-tag'; import { DisablePublicSharing, MoveToTrash } from './operation-menu-items'; import * as styles from './page-list.css'; diff --git a/packages/frontend/component/src/components/page-list/operation-menu-items/disable-public-sharing.tsx b/packages/frontend/core/src/components/page-list/operation-menu-items/disable-public-sharing.tsx similarity index 75% rename from packages/frontend/component/src/components/page-list/operation-menu-items/disable-public-sharing.tsx rename to packages/frontend/core/src/components/page-list/operation-menu-items/disable-public-sharing.tsx index 7456e72217..df1f14f0e9 100644 --- a/packages/frontend/component/src/components/page-list/operation-menu-items/disable-public-sharing.tsx +++ b/packages/frontend/core/src/components/page-list/operation-menu-items/disable-public-sharing.tsx @@ -1,9 +1,8 @@ +import { MenuIcon, MenuItem, type MenuItemProps } from '@affine/component'; +import { PublicLinkDisableModal } from '@affine/component/disable-public-link'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { ShareIcon } from '@blocksuite/icons'; -import { MenuIcon, MenuItem, type MenuItemProps } from '../../../ui/menu'; -import { PublicLinkDisableModal } from '../../disable-public-link'; - export const DisablePublicSharing = (props: MenuItemProps) => { const t = useAFFiNEI18N(); return ( diff --git a/packages/frontend/component/src/components/page-list/operation-menu-items/export.tsx b/packages/frontend/core/src/components/page-list/operation-menu-items/export.tsx similarity index 97% rename from packages/frontend/component/src/components/page-list/operation-menu-items/export.tsx rename to packages/frontend/core/src/components/page-list/operation-menu-items/export.tsx index 939fd9f1ea..4aa669aad2 100644 --- a/packages/frontend/component/src/components/page-list/operation-menu-items/export.tsx +++ b/packages/frontend/core/src/components/page-list/operation-menu-items/export.tsx @@ -1,3 +1,4 @@ +import { MenuIcon, MenuItem, MenuSub } from '@affine/component'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { ExportIcon, @@ -8,7 +9,6 @@ import { } from '@blocksuite/icons'; import { type ReactNode, useMemo } from 'react'; -import { MenuIcon, MenuItem, MenuSub } from '../../../ui/menu'; import { transitionStyle } from './index.css'; interface ExportMenuItemProps { diff --git a/packages/frontend/component/src/components/page-list/operation-menu-items/get-content-parser.ts b/packages/frontend/core/src/components/page-list/operation-menu-items/get-content-parser.ts similarity index 100% rename from packages/frontend/component/src/components/page-list/operation-menu-items/get-content-parser.ts rename to packages/frontend/core/src/components/page-list/operation-menu-items/get-content-parser.ts diff --git a/packages/frontend/component/src/components/page-list/operation-menu-items/index.css.ts b/packages/frontend/core/src/components/page-list/operation-menu-items/index.css.ts similarity index 100% rename from packages/frontend/component/src/components/page-list/operation-menu-items/index.css.ts rename to packages/frontend/core/src/components/page-list/operation-menu-items/index.css.ts diff --git a/packages/frontend/component/src/components/page-list/operation-menu-items/index.ts b/packages/frontend/core/src/components/page-list/operation-menu-items/index.ts similarity index 100% rename from packages/frontend/component/src/components/page-list/operation-menu-items/index.ts rename to packages/frontend/core/src/components/page-list/operation-menu-items/index.ts diff --git a/packages/frontend/component/src/components/page-list/operation-menu-items/move-to-trash.tsx b/packages/frontend/core/src/components/page-list/operation-menu-items/move-to-trash.tsx similarity index 90% rename from packages/frontend/component/src/components/page-list/operation-menu-items/move-to-trash.tsx rename to packages/frontend/core/src/components/page-list/operation-menu-items/move-to-trash.tsx index 82abf8c15d..4f39f5d7a1 100644 --- a/packages/frontend/component/src/components/page-list/operation-menu-items/move-to-trash.tsx +++ b/packages/frontend/core/src/components/page-list/operation-menu-items/move-to-trash.tsx @@ -1,9 +1,13 @@ +import { + ConfirmModal, + type ConfirmModalProps, + MenuIcon, + MenuItem, + type MenuItemProps, +} from '@affine/component'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { DeleteIcon } from '@blocksuite/icons'; -import { MenuIcon, MenuItem, type MenuItemProps } from '../../../ui/menu'; -import { ConfirmModal, type ConfirmModalProps } from '../../../ui/modal'; - export const MoveToTrash = (props: MenuItemProps) => { const t = useAFFiNEI18N(); diff --git a/packages/frontend/component/src/components/page-list/operation-menu-items/types.ts b/packages/frontend/core/src/components/page-list/operation-menu-items/types.ts similarity index 100% rename from packages/frontend/component/src/components/page-list/operation-menu-items/types.ts rename to packages/frontend/core/src/components/page-list/operation-menu-items/types.ts diff --git a/packages/frontend/component/src/components/page-list/page-content-preview.tsx b/packages/frontend/core/src/components/page-list/page-content-preview.tsx similarity index 100% rename from packages/frontend/component/src/components/page-list/page-content-preview.tsx rename to packages/frontend/core/src/components/page-list/page-content-preview.tsx diff --git a/packages/frontend/component/src/components/page-list/page-group.css.ts b/packages/frontend/core/src/components/page-list/page-group.css.ts similarity index 100% rename from packages/frontend/component/src/components/page-list/page-group.css.ts rename to packages/frontend/core/src/components/page-list/page-group.css.ts diff --git a/packages/frontend/component/src/components/page-list/page-group.tsx b/packages/frontend/core/src/components/page-list/page-group.tsx similarity index 100% rename from packages/frontend/component/src/components/page-list/page-group.tsx rename to packages/frontend/core/src/components/page-list/page-group.tsx diff --git a/packages/frontend/component/src/components/page-list/page-header.tsx b/packages/frontend/core/src/components/page-list/page-header.tsx similarity index 98% rename from packages/frontend/component/src/components/page-list/page-header.tsx rename to packages/frontend/core/src/components/page-list/page-header.tsx index c69aae3137..a1f7e39c75 100644 --- a/packages/frontend/component/src/components/page-list/page-header.tsx +++ b/packages/frontend/core/src/components/page-list/page-header.tsx @@ -1,3 +1,4 @@ +import { Checkbox, type CheckboxProps } from '@affine/component'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { MultiSelectIcon, SortDownIcon, SortUpIcon } from '@blocksuite/icons'; import type { PageMeta } from '@blocksuite/store'; @@ -10,7 +11,6 @@ import { useMemo, } from 'react'; -import { Checkbox, type CheckboxProps } from '../../ui/checkbox'; import * as styles from './page-list.css'; import { pageListHandlersAtom, diff --git a/packages/frontend/component/src/components/page-list/page-list-item.css.ts b/packages/frontend/core/src/components/page-list/page-list-item.css.ts similarity index 100% rename from packages/frontend/component/src/components/page-list/page-list-item.css.ts rename to packages/frontend/core/src/components/page-list/page-list-item.css.ts diff --git a/packages/frontend/component/src/components/page-list/page-list-item.tsx b/packages/frontend/core/src/components/page-list/page-list-item.tsx similarity index 99% rename from packages/frontend/component/src/components/page-list/page-list-item.tsx rename to packages/frontend/core/src/components/page-list/page-list-item.tsx index ce55e32ac7..775b86be8d 100644 --- a/packages/frontend/component/src/components/page-list/page-list-item.tsx +++ b/packages/frontend/core/src/components/page-list/page-list-item.tsx @@ -1,9 +1,9 @@ +import { Checkbox } from '@affine/component'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useDraggable } from '@dnd-kit/core'; import { type PropsWithChildren, useCallback, useMemo } from 'react'; import { Link } from 'react-router-dom'; -import { Checkbox } from '../../ui/checkbox'; import * as styles from './page-list-item.css'; import { PageTags } from './page-tags'; import type { DraggableTitleCellData, PageListItemProps } from './types'; diff --git a/packages/frontend/component/src/components/page-list/page-list.css.ts b/packages/frontend/core/src/components/page-list/page-list.css.ts similarity index 100% rename from packages/frontend/component/src/components/page-list/page-list.css.ts rename to packages/frontend/core/src/components/page-list/page-list.css.ts diff --git a/packages/frontend/component/src/components/page-list/page-list.tsx b/packages/frontend/core/src/components/page-list/page-list.tsx similarity index 97% rename from packages/frontend/component/src/components/page-list/page-list.tsx rename to packages/frontend/core/src/components/page-list/page-list.tsx index 218d572d86..58420f2593 100644 --- a/packages/frontend/component/src/components/page-list/page-list.tsx +++ b/packages/frontend/core/src/components/page-list/page-list.tsx @@ -1,3 +1,5 @@ +import { Scrollable } from '@affine/component'; +import { useHasScrollTop } from '@affine/component/app-sidebar'; import clsx from 'clsx'; import { type ForwardedRef, @@ -10,8 +12,6 @@ import { useRef, } from 'react'; -import { Scrollable } from '../../ui/scrollbar'; -import { useHasScrollTop } from '../app-sidebar/sidebar-containers/use-has-scroll-top'; import { PageGroup } from './page-group'; import { PageListTableHeader } from './page-header'; import * as styles from './page-list.css'; diff --git a/packages/frontend/component/src/components/page-list/page-tags.css.ts b/packages/frontend/core/src/components/page-list/page-tags.css.ts similarity index 100% rename from packages/frontend/component/src/components/page-list/page-tags.css.ts rename to packages/frontend/core/src/components/page-list/page-tags.css.ts diff --git a/packages/frontend/component/src/components/page-list/page-tags.tsx b/packages/frontend/core/src/components/page-list/page-tags.tsx similarity index 98% rename from packages/frontend/component/src/components/page-list/page-tags.tsx rename to packages/frontend/core/src/components/page-list/page-tags.tsx index 7fb86e9dce..994e91bdf9 100644 --- a/packages/frontend/component/src/components/page-list/page-tags.tsx +++ b/packages/frontend/core/src/components/page-list/page-tags.tsx @@ -1,10 +1,10 @@ +import { Menu } from '@affine/component'; import type { Tag } from '@affine/env/filter'; import { MoreHorizontalIcon } from '@blocksuite/icons'; import { assignInlineVars } from '@vanilla-extract/dynamic'; import clsx from 'clsx'; import { useMemo } from 'react'; -import { Menu } from '../../ui/menu'; import * as styles from './page-tags.css'; import { stopPropagation } from './utils'; diff --git a/packages/frontend/component/src/components/page-list/pages-to-page-group.tsx b/packages/frontend/core/src/components/page-list/pages-to-page-group.tsx similarity index 100% rename from packages/frontend/component/src/components/page-list/pages-to-page-group.tsx rename to packages/frontend/core/src/components/page-list/pages-to-page-group.tsx diff --git a/packages/frontend/component/src/components/page-list/readme.md b/packages/frontend/core/src/components/page-list/readme.md similarity index 100% rename from packages/frontend/component/src/components/page-list/readme.md rename to packages/frontend/core/src/components/page-list/readme.md diff --git a/packages/frontend/component/src/components/page-list/scoped-atoms.tsx b/packages/frontend/core/src/components/page-list/scoped-atoms.tsx similarity index 100% rename from packages/frontend/component/src/components/page-list/scoped-atoms.tsx rename to packages/frontend/core/src/components/page-list/scoped-atoms.tsx diff --git a/packages/frontend/component/src/components/page-list/types.ts b/packages/frontend/core/src/components/page-list/types.ts similarity index 100% rename from packages/frontend/component/src/components/page-list/types.ts rename to packages/frontend/core/src/components/page-list/types.ts diff --git a/packages/frontend/component/src/components/page-list/use-block-suite-page-preview.ts b/packages/frontend/core/src/components/page-list/use-block-suite-page-preview.ts similarity index 100% rename from packages/frontend/component/src/components/page-list/use-block-suite-page-preview.ts rename to packages/frontend/core/src/components/page-list/use-block-suite-page-preview.ts diff --git a/packages/frontend/component/src/components/page-list/use-block-suite-workspace-page.ts b/packages/frontend/core/src/components/page-list/use-block-suite-workspace-page.ts similarity index 100% rename from packages/frontend/component/src/components/page-list/use-block-suite-workspace-page.ts rename to packages/frontend/core/src/components/page-list/use-block-suite-workspace-page.ts diff --git a/packages/frontend/component/src/components/page-list/use-collection-manager.ts b/packages/frontend/core/src/components/page-list/use-collection-manager.ts similarity index 100% rename from packages/frontend/component/src/components/page-list/use-collection-manager.ts rename to packages/frontend/core/src/components/page-list/use-collection-manager.ts diff --git a/packages/frontend/component/src/components/page-list/use-page-selection-events.ts b/packages/frontend/core/src/components/page-list/use-page-selection-events.ts similarity index 100% rename from packages/frontend/component/src/components/page-list/use-page-selection-events.ts rename to packages/frontend/core/src/components/page-list/use-page-selection-events.ts diff --git a/packages/frontend/component/src/components/page-list/utils.tsx b/packages/frontend/core/src/components/page-list/utils.tsx similarity index 100% rename from packages/frontend/component/src/components/page-list/utils.tsx rename to packages/frontend/core/src/components/page-list/utils.tsx diff --git a/packages/frontend/component/src/components/page-list/view/affine-shape.tsx b/packages/frontend/core/src/components/page-list/view/affine-shape.tsx similarity index 100% rename from packages/frontend/component/src/components/page-list/view/affine-shape.tsx rename to packages/frontend/core/src/components/page-list/view/affine-shape.tsx diff --git a/packages/frontend/component/src/components/page-list/view/collection-bar.css.ts b/packages/frontend/core/src/components/page-list/view/collection-bar.css.ts similarity index 100% rename from packages/frontend/component/src/components/page-list/view/collection-bar.css.ts rename to packages/frontend/core/src/components/page-list/view/collection-bar.css.ts diff --git a/packages/frontend/component/src/components/page-list/view/collection-bar.tsx b/packages/frontend/core/src/components/page-list/view/collection-bar.tsx similarity index 97% rename from packages/frontend/component/src/components/page-list/view/collection-bar.tsx rename to packages/frontend/core/src/components/page-list/view/collection-bar.tsx index 2b8c102158..ec8f1daf05 100644 --- a/packages/frontend/component/src/components/page-list/view/collection-bar.tsx +++ b/packages/frontend/core/src/components/page-list/view/collection-bar.tsx @@ -1,3 +1,4 @@ +import { Button, Tooltip } from '@affine/component'; import type { DeleteCollectionInfo, PropertiesMeta } from '@affine/env/filter'; import type { GetPageInfoById } from '@affine/env/page-info'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; @@ -5,8 +6,6 @@ import { ViewLayersIcon } from '@blocksuite/icons'; import clsx from 'clsx'; import { useState } from 'react'; -import { Button } from '../../../ui/button'; -import { Tooltip } from '../../../ui/tooltip'; import { type CollectionsCRUDAtom, useCollectionManager, diff --git a/packages/frontend/component/src/components/page-list/view/collection-list.css.ts b/packages/frontend/core/src/components/page-list/view/collection-list.css.ts similarity index 100% rename from packages/frontend/component/src/components/page-list/view/collection-list.css.ts rename to packages/frontend/core/src/components/page-list/view/collection-list.css.ts diff --git a/packages/frontend/component/src/components/page-list/view/collection-list.tsx b/packages/frontend/core/src/components/page-list/view/collection-list.tsx similarity index 95% rename from packages/frontend/component/src/components/page-list/view/collection-list.tsx rename to packages/frontend/core/src/components/page-list/view/collection-list.tsx index 2bede93aa0..2a99409c3b 100644 --- a/packages/frontend/component/src/components/page-list/view/collection-list.tsx +++ b/packages/frontend/core/src/components/page-list/view/collection-list.tsx @@ -1,3 +1,6 @@ +import { Button } from '@affine/component'; +import { FlexWrapper } from '@affine/component'; +import { Menu } from '@affine/component'; import type { Collection, DeleteCollectionInfo, @@ -8,9 +11,6 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { FilterIcon } from '@blocksuite/icons'; import { useCallback, useState } from 'react'; -import { Button } from '../../../ui/button'; -import { FlexWrapper } from '../../../ui/layout'; -import { Menu } from '../../../ui/menu'; import { CreateFilterMenu } from '../filter/vars'; import type { useCollectionManager } from '../use-collection-manager'; import * as styles from './collection-list.css'; diff --git a/packages/frontend/component/src/components/page-list/view/collection-operations.css.ts b/packages/frontend/core/src/components/page-list/view/collection-operations.css.ts similarity index 100% rename from packages/frontend/component/src/components/page-list/view/collection-operations.css.ts rename to packages/frontend/core/src/components/page-list/view/collection-operations.css.ts diff --git a/packages/frontend/component/src/components/page-list/view/collection-operations.tsx b/packages/frontend/core/src/components/page-list/view/collection-operations.tsx similarity index 97% rename from packages/frontend/component/src/components/page-list/view/collection-operations.tsx rename to packages/frontend/core/src/components/page-list/view/collection-operations.tsx index 97cac902ef..a8829cb5cf 100644 --- a/packages/frontend/component/src/components/page-list/view/collection-operations.tsx +++ b/packages/frontend/core/src/components/page-list/view/collection-operations.tsx @@ -1,3 +1,9 @@ +import { + Menu, + MenuIcon, + MenuItem, + type MenuItemProps, +} from '@affine/component'; import type { Collection, DeleteCollectionInfo } from '@affine/env/filter'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { DeleteIcon, EditIcon, FilterIcon } from '@blocksuite/icons'; @@ -8,7 +14,6 @@ import { useMemo, } from 'react'; -import { Menu, MenuIcon, MenuItem, type MenuItemProps } from '../../../ui/menu'; import type { useCollectionManager } from '../use-collection-manager'; import type { AllPageListConfig } from '.'; import * as styles from './collection-operations.css'; diff --git a/packages/frontend/component/src/components/page-list/view/create-collection.css.ts b/packages/frontend/core/src/components/page-list/view/create-collection.css.ts similarity index 100% rename from packages/frontend/component/src/components/page-list/view/create-collection.css.ts rename to packages/frontend/core/src/components/page-list/view/create-collection.css.ts diff --git a/packages/frontend/component/src/components/page-list/view/create-collection.tsx b/packages/frontend/core/src/components/page-list/view/create-collection.tsx similarity index 95% rename from packages/frontend/component/src/components/page-list/view/create-collection.tsx rename to packages/frontend/core/src/components/page-list/view/create-collection.tsx index 5046eaeda1..34083054d0 100644 --- a/packages/frontend/component/src/components/page-list/view/create-collection.tsx +++ b/packages/frontend/core/src/components/page-list/view/create-collection.tsx @@ -1,9 +1,7 @@ +import { Button, Input, Modal } from '@affine/component'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useCallback, useMemo, useState } from 'react'; -import { Button } from '../../../ui/button'; -import Input from '../../../ui/input'; -import { Modal } from '../../../ui/modal'; import * as styles from './create-collection.css'; export interface CreateCollectionModalProps { diff --git a/packages/frontend/component/src/components/page-list/view/edit-collection/edit-collection.css.ts b/packages/frontend/core/src/components/page-list/view/edit-collection/edit-collection.css.ts similarity index 100% rename from packages/frontend/component/src/components/page-list/view/edit-collection/edit-collection.css.ts rename to packages/frontend/core/src/components/page-list/view/edit-collection/edit-collection.css.ts diff --git a/packages/frontend/component/src/components/page-list/view/edit-collection/edit-collection.tsx b/packages/frontend/core/src/components/page-list/view/edit-collection/edit-collection.tsx similarity index 97% rename from packages/frontend/component/src/components/page-list/view/edit-collection/edit-collection.tsx rename to packages/frontend/core/src/components/page-list/view/edit-collection/edit-collection.tsx index b96a0ad448..895431fd74 100644 --- a/packages/frontend/component/src/components/page-list/view/edit-collection/edit-collection.tsx +++ b/packages/frontend/core/src/components/page-list/view/edit-collection/edit-collection.tsx @@ -1,12 +1,15 @@ +import { + Button, + Modal, + RadioButton, + RadioButtonGroup, +} from '@affine/component'; import type { Collection } from '@affine/env/filter'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import type { PageMeta, Workspace } from '@blocksuite/store'; import type { DialogContentProps } from '@radix-ui/react-dialog'; import { type ReactNode, useCallback, useMemo, useState } from 'react'; -import { RadioButton, RadioButtonGroup } from '../../../../index'; -import { Button } from '../../../../ui/button'; -import { Modal } from '../../../../ui/modal'; import * as styles from './edit-collection.css'; import { PagesMode } from './pages-mode'; import { RulesMode } from './rules-mode'; diff --git a/packages/frontend/component/src/components/page-list/view/edit-collection/hooks.tsx b/packages/frontend/core/src/components/page-list/view/edit-collection/hooks.tsx similarity index 96% rename from packages/frontend/component/src/components/page-list/view/edit-collection/hooks.tsx rename to packages/frontend/core/src/components/page-list/view/edit-collection/hooks.tsx index 538edffb4b..181449c496 100644 --- a/packages/frontend/component/src/components/page-list/view/edit-collection/hooks.tsx +++ b/packages/frontend/core/src/components/page-list/view/edit-collection/hooks.tsx @@ -1,6 +1,6 @@ +import { Modal } from '@affine/component'; import { useCallback, useState } from 'react'; -import { Modal } from '../../../../ui/modal'; import type { AllPageListConfig } from './edit-collection'; import { SelectPage } from './select-page'; export const useSelectPage = ({ diff --git a/packages/frontend/component/src/components/page-list/view/edit-collection/pages-mode.tsx b/packages/frontend/core/src/components/page-list/view/edit-collection/pages-mode.tsx similarity index 99% rename from packages/frontend/component/src/components/page-list/view/edit-collection/pages-mode.tsx rename to packages/frontend/core/src/components/page-list/view/edit-collection/pages-mode.tsx index d04a681e0c..f13d99599c 100644 --- a/packages/frontend/component/src/components/page-list/view/edit-collection/pages-mode.tsx +++ b/packages/frontend/core/src/components/page-list/view/edit-collection/pages-mode.tsx @@ -1,3 +1,4 @@ +import { Menu } from '@affine/component'; import type { Collection } from '@affine/env/filter'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { FilterIcon } from '@blocksuite/icons'; @@ -5,7 +6,6 @@ import type { PageMeta } from '@blocksuite/store'; import clsx from 'clsx'; import { type ReactNode, useCallback } from 'react'; -import { Menu } from '../../../../ui/menu'; import { FilterList } from '../../filter/filter-list'; import { VariableSelect } from '../../filter/vars'; import { VirtualizedPageList } from '../../virtualized-page-list'; diff --git a/packages/frontend/component/src/components/page-list/view/edit-collection/rules-mode.tsx b/packages/frontend/core/src/components/page-list/view/edit-collection/rules-mode.tsx similarity index 100% rename from packages/frontend/component/src/components/page-list/view/edit-collection/rules-mode.tsx rename to packages/frontend/core/src/components/page-list/view/edit-collection/rules-mode.tsx diff --git a/packages/frontend/component/src/components/page-list/view/edit-collection/select-page.tsx b/packages/frontend/core/src/components/page-list/view/edit-collection/select-page.tsx similarity index 98% rename from packages/frontend/component/src/components/page-list/view/edit-collection/select-page.tsx rename to packages/frontend/core/src/components/page-list/view/edit-collection/select-page.tsx index 28616a4df1..7debf879c0 100644 --- a/packages/frontend/component/src/components/page-list/view/edit-collection/select-page.tsx +++ b/packages/frontend/core/src/components/page-list/view/edit-collection/select-page.tsx @@ -1,11 +1,10 @@ +import { Button, Menu } from '@affine/component'; import { Trans } from '@affine/i18n'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { FilterIcon } from '@blocksuite/icons'; import clsx from 'clsx'; import { useCallback, useState } from 'react'; -import { Button } from '../../../../ui/button'; -import { Menu } from '../../../../ui/menu'; import { FilterList } from '../../filter'; import { VariableSelect } from '../../filter/vars'; import { VirtualizedPageList } from '../../virtualized-page-list'; diff --git a/packages/frontend/component/src/components/page-list/view/edit-collection/use-filter.tsx b/packages/frontend/core/src/components/page-list/view/edit-collection/use-filter.tsx similarity index 100% rename from packages/frontend/component/src/components/page-list/view/edit-collection/use-filter.tsx rename to packages/frontend/core/src/components/page-list/view/edit-collection/use-filter.tsx diff --git a/packages/frontend/component/src/components/page-list/view/edit-collection/use-search.tsx b/packages/frontend/core/src/components/page-list/view/edit-collection/use-search.tsx similarity index 100% rename from packages/frontend/component/src/components/page-list/view/edit-collection/use-search.tsx rename to packages/frontend/core/src/components/page-list/view/edit-collection/use-search.tsx diff --git a/packages/frontend/component/src/components/page-list/view/index.ts b/packages/frontend/core/src/components/page-list/view/index.ts similarity index 100% rename from packages/frontend/component/src/components/page-list/view/index.ts rename to packages/frontend/core/src/components/page-list/view/index.ts diff --git a/packages/frontend/component/src/components/page-list/view/save-as-collection-button.tsx b/packages/frontend/core/src/components/page-list/view/save-as-collection-button.tsx similarity index 96% rename from packages/frontend/component/src/components/page-list/view/save-as-collection-button.tsx rename to packages/frontend/core/src/components/page-list/view/save-as-collection-button.tsx index eec63d4e4f..187308ba88 100644 --- a/packages/frontend/component/src/components/page-list/view/save-as-collection-button.tsx +++ b/packages/frontend/core/src/components/page-list/view/save-as-collection-button.tsx @@ -1,10 +1,10 @@ +import { Button } from '@affine/component'; import type { Collection } from '@affine/env/filter'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { SaveIcon } from '@blocksuite/icons'; import { nanoid } from 'nanoid'; import { useCallback } from 'react'; -import { Button } from '../../../ui/button'; import { createEmptyCollection } from '../use-collection-manager'; import { useEditCollectionName } from './use-edit-collection'; diff --git a/packages/frontend/component/src/components/page-list/view/use-action.tsx b/packages/frontend/core/src/components/page-list/view/use-action.tsx similarity index 100% rename from packages/frontend/component/src/components/page-list/view/use-action.tsx rename to packages/frontend/core/src/components/page-list/view/use-action.tsx diff --git a/packages/frontend/component/src/components/page-list/view/use-edit-collection.tsx b/packages/frontend/core/src/components/page-list/view/use-edit-collection.tsx similarity index 100% rename from packages/frontend/component/src/components/page-list/view/use-edit-collection.tsx rename to packages/frontend/core/src/components/page-list/view/use-edit-collection.tsx diff --git a/packages/frontend/component/src/components/page-list/virtualized-page-list.tsx b/packages/frontend/core/src/components/page-list/virtualized-page-list.tsx similarity index 99% rename from packages/frontend/component/src/components/page-list/virtualized-page-list.tsx rename to packages/frontend/core/src/components/page-list/virtualized-page-list.tsx index 40fc9f8637..baff371df5 100644 --- a/packages/frontend/component/src/components/page-list/virtualized-page-list.tsx +++ b/packages/frontend/core/src/components/page-list/virtualized-page-list.tsx @@ -1,3 +1,4 @@ +import { Scrollable } from '@affine/component'; import type { PageMeta } from '@blocksuite/store'; import clsx from 'clsx'; import { selectAtom } from 'jotai/utils'; @@ -11,7 +12,6 @@ import { } from 'react'; import { Virtuoso } from 'react-virtuoso'; -import { Scrollable } from '../../ui/scrollbar'; import { PageGroupHeader, PageMetaListItemRenderer } from './page-group'; import { PageListTableHeader } from './page-header'; import { PageListInnerWrapper } from './page-list'; diff --git a/packages/frontend/core/src/components/pure/cmdk/data.tsx b/packages/frontend/core/src/components/pure/cmdk/data.tsx index 0bec9f4ac8..3af7eb84ca 100644 --- a/packages/frontend/core/src/components/pure/cmdk/data.tsx +++ b/packages/frontend/core/src/components/pure/cmdk/data.tsx @@ -1,4 +1,4 @@ -import { useCollectionManager } from '@affine/component/page-list'; +import { useCollectionManager } from '@affine/core/components/page-list'; import { useBlockSuitePageMeta, usePageMetaHelper, diff --git a/packages/frontend/core/src/components/pure/cmdk/main.tsx b/packages/frontend/core/src/components/pure/cmdk/main.tsx index 4a28ca08d3..dc6423952b 100644 --- a/packages/frontend/core/src/components/pure/cmdk/main.tsx +++ b/packages/frontend/core/src/components/pure/cmdk/main.tsx @@ -1,4 +1,4 @@ -import { formatDate } from '@affine/component/page-list'; +import { formatDate } from '@affine/core/components/page-list'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import type { PageMeta } from '@blocksuite/store'; diff --git a/packages/frontend/core/src/components/pure/workspace-slider-bar/collections/collections-list.tsx b/packages/frontend/core/src/components/pure/workspace-slider-bar/collections/collections-list.tsx index 5f40e87fe7..af8bc3bbbb 100644 --- a/packages/frontend/core/src/components/pure/workspace-slider-bar/collections/collections-list.tsx +++ b/packages/frontend/core/src/components/pure/workspace-slider-bar/collections/collections-list.tsx @@ -1,14 +1,14 @@ import { AnimatedCollectionsIcon, toast } from '@affine/component'; import { MenuLinkItem as SidebarMenuLinkItem } from '@affine/component/app-sidebar'; +import { RenameModal } from '@affine/component/rename-modal'; +import { Button, IconButton } from '@affine/component/ui/button'; import { CollectionOperations, filterPage, stopPropagation, useCollectionManager, useSavedCollections, -} from '@affine/component/page-list'; -import { RenameModal } from '@affine/component/rename-modal'; -import { Button, IconButton } from '@affine/component/ui/button'; +} from '@affine/core/components/page-list'; import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta'; import type { Collection, DeleteCollectionInfo } from '@affine/env/filter'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; diff --git a/packages/frontend/core/src/components/root-app-sidebar/index.tsx b/packages/frontend/core/src/components/root-app-sidebar/index.tsx index 19dafd5f22..ca4b1e98e8 100644 --- a/packages/frontend/core/src/components/root-app-sidebar/index.tsx +++ b/packages/frontend/core/src/components/root-app-sidebar/index.tsx @@ -11,12 +11,6 @@ import { SidebarContainer, SidebarScrollableContainer, } from '@affine/component/app-sidebar'; -import { - createEmptyCollection, - MoveToTrash, - useCollectionManager, - useEditCollectionName, -} from '@affine/component/page-list'; import { Menu } from '@affine/component/ui/menu'; import { collectionsCRUDAtom } from '@affine/core/atoms/collections'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; @@ -41,6 +35,12 @@ import { getDropItemId } from '../../hooks/affine/use-sidebar-drag'; import { useTrashModalHelper } from '../../hooks/affine/use-trash-modal-helper'; import { useRegisterBrowserHistoryCommands } from '../../hooks/use-browser-history-commands'; import { useNavigateHelper } from '../../hooks/use-navigate-helper'; +import { + createEmptyCollection, + MoveToTrash, + useCollectionManager, + useEditCollectionName, +} from '../page-list'; import { CollectionsList } from '../pure/workspace-slider-bar/collections'; import { AddCollectionButton } from '../pure/workspace-slider-bar/collections/add-collection-button'; import { AddFavouriteButton } from '../pure/workspace-slider-bar/favorite/add-favourite-button'; diff --git a/packages/frontend/core/src/components/workspace-upgrade/upgrade.tsx b/packages/frontend/core/src/components/workspace-upgrade/upgrade.tsx index a93fb73dce..830d5225d5 100644 --- a/packages/frontend/core/src/components/workspace-upgrade/upgrade.tsx +++ b/packages/frontend/core/src/components/workspace-upgrade/upgrade.tsx @@ -1,5 +1,5 @@ -import { AffineShapeIcon } from '@affine/component/page-list'; // TODO: import from page-list temporarily, need to defined common svg icon/images management. import { Button } from '@affine/component/ui/button'; +import { AffineShapeIcon } from '@affine/core/components/page-list'; // TODO: import from page-list temporarily, need to defined common svg icon/images management. import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper'; import { useWorkspaceStatus } from '@affine/core/hooks/use-workspace-status'; diff --git a/packages/frontend/core/src/hooks/affine/use-all-page-list-config.tsx b/packages/frontend/core/src/hooks/affine/use-all-page-list-config.tsx index 803992507c..b263b64f57 100644 --- a/packages/frontend/core/src/hooks/affine/use-all-page-list-config.tsx +++ b/packages/frontend/core/src/hooks/affine/use-all-page-list-config.tsx @@ -2,7 +2,7 @@ import { toast } from '@affine/component'; import { type AllPageListConfig, FavoriteTag, -} from '@affine/component/page-list'; +} from '@affine/core/components/page-list'; import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta'; import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; diff --git a/packages/frontend/core/src/hooks/affine/use-sidebar-drag.ts b/packages/frontend/core/src/hooks/affine/use-sidebar-drag.ts index 0ad0231dc1..2065117e80 100644 --- a/packages/frontend/core/src/hooks/affine/use-sidebar-drag.ts +++ b/packages/frontend/core/src/hooks/affine/use-sidebar-drag.ts @@ -1,5 +1,5 @@ import { toast } from '@affine/component'; -import type { DraggableTitleCellData } from '@affine/component/page-list'; +import type { DraggableTitleCellData } from '@affine/core/components/page-list'; import { usePageMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta'; import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; diff --git a/packages/frontend/core/src/layouts/workspace-layout.tsx b/packages/frontend/core/src/layouts/workspace-layout.tsx index 17fe3ba50a..f3bbf655bb 100644 --- a/packages/frontend/core/src/layouts/workspace-layout.tsx +++ b/packages/frontend/core/src/layouts/workspace-layout.tsx @@ -2,10 +2,6 @@ import { AppSidebarFallback, appSidebarResizingAtom, } from '@affine/component/app-sidebar'; -import { - type DraggableTitleCellData, - PageListDragOverlay, -} from '@affine/component/page-list'; import { MainContainer, WorkspaceFallback } from '@affine/component/workspace'; import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta'; import { useWorkspaceStatus } from '@affine/core/hooks/use-workspace-status'; @@ -30,6 +26,10 @@ import { openQuickSearchModalAtom, openSettingModalAtom } from '../atoms'; import { AppContainer } from '../components/affine/app-container'; import { SyncAwareness } from '../components/affine/awareness'; import { usePageHelper } from '../components/blocksuite/block-suite-page-list/utils'; +import { + type DraggableTitleCellData, + PageListDragOverlay, +} from '../components/page-list'; import { RootAppSidebar } from '../components/root-app-sidebar'; import { WorkspaceUpgrade } from '../components/workspace-upgrade'; import { useAppSettingHelper } from '../hooks/affine/use-app-setting-helper'; diff --git a/packages/frontend/core/src/pages/workspace/all-page/all-page-filter.tsx b/packages/frontend/core/src/pages/workspace/all-page/all-page-filter.tsx index e3d302cafd..fa006f4c59 100644 --- a/packages/frontend/core/src/pages/workspace/all-page/all-page-filter.tsx +++ b/packages/frontend/core/src/pages/workspace/all-page/all-page-filter.tsx @@ -1,8 +1,3 @@ -import { - FilterList, - SaveAsCollectionButton, - useCollectionManager, -} from '@affine/component/page-list'; import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace'; import type { Collection, Filter } from '@affine/env/filter'; import { useAtomValue } from 'jotai'; @@ -10,6 +5,11 @@ import { useCallback } from 'react'; import { collectionsCRUDAtom } from '../../../atoms/collections'; import { filterContainerStyle } from '../../../components/filter-container.css'; +import { + FilterList, + SaveAsCollectionButton, + useCollectionManager, +} from '../../../components/page-list'; import { useNavigateHelper } from '../../../hooks/use-navigate-helper'; export const FilterContainer = () => { diff --git a/packages/frontend/core/src/pages/workspace/all-page/all-page.tsx b/packages/frontend/core/src/pages/workspace/all-page/all-page.tsx index fed910d907..11cd36e6f5 100644 --- a/packages/frontend/core/src/pages/workspace/all-page/all-page.tsx +++ b/packages/frontend/core/src/pages/workspace/all-page/all-page.tsx @@ -8,7 +8,7 @@ import { type PageListHandle, useCollectionManager, VirtualizedPageList, -} from '@affine/component/page-list'; +} from '@affine/core/components/page-list'; import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta'; import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace'; import { Trans } from '@affine/i18n'; diff --git a/packages/frontend/core/src/pages/workspace/collection.tsx b/packages/frontend/core/src/pages/workspace/collection.tsx index 903e06415a..446ea36edb 100644 --- a/packages/frontend/core/src/pages/workspace/collection.tsx +++ b/packages/frontend/core/src/pages/workspace/collection.tsx @@ -8,7 +8,7 @@ import { currentCollectionAtom, useCollectionManager, useEditCollection, -} from '@affine/component/page-list'; +} from '@affine/core/components/page-list'; import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace'; diff --git a/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx b/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx index 72e064940c..79d7de919b 100644 --- a/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx +++ b/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx @@ -1,8 +1,4 @@ import { PageDetailSkeleton } from '@affine/component/page-detail-skeleton'; -import { - createTagFilter, - useCollectionManager, -} from '@affine/component/page-list'; import { ResizePanel } from '@affine/component/resize-panel'; import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta'; import { useWorkspaceStatus } from '@affine/core/hooks/use-workspace-status'; @@ -32,6 +28,10 @@ import { HubIsland } from '../../../components/affine/hub-island'; import { GlobalPageHistoryModal } from '../../../components/affine/page-history-modal'; import { ImagePreviewModal } from '../../../components/image-preview'; import { PageDetailEditor } from '../../../components/page-detail-editor'; +import { + createTagFilter, + useCollectionManager, +} from '../../../components/page-list'; import { TrashPageFooter } from '../../../components/pure/trash-page-footer'; import { TopTip } from '../../../components/top-tip'; import { useRegisterBlocksuiteEditorCommands } from '../../../hooks/affine/use-register-blocksuite-editor-commands'; diff --git a/packages/frontend/core/src/pages/workspace/pages.tsx b/packages/frontend/core/src/pages/workspace/pages.tsx index 13938a21d1..ecb689923b 100644 --- a/packages/frontend/core/src/pages/workspace/pages.tsx +++ b/packages/frontend/core/src/pages/workspace/pages.tsx @@ -2,7 +2,7 @@ import { filterPage, filterPageByRules, useCollectionManager, -} from '@affine/component/page-list'; +} from '@affine/core/components/page-list'; import type { PageMeta } from '@blocksuite/store'; import { useAtomValue } from 'jotai'; import { useMemo } from 'react'; diff --git a/packages/frontend/core/src/pages/workspace/trash-page.tsx b/packages/frontend/core/src/pages/workspace/trash-page.tsx index 71eaa01749..d5b4540c53 100644 --- a/packages/frontend/core/src/pages/workspace/trash-page.tsx +++ b/packages/frontend/core/src/pages/workspace/trash-page.tsx @@ -3,7 +3,7 @@ import { currentCollectionAtom, TrashOperationCell, VirtualizedPageList, -} from '@affine/component/page-list'; +} from '@affine/core/components/page-list'; import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta'; import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; diff --git a/tests/storybook/project.json b/tests/storybook/project.json index 5e2fd3e119..9415509751 100644 --- a/tests/storybook/project.json +++ b/tests/storybook/project.json @@ -9,6 +9,7 @@ "default", "^production", "{projectRoot}/.storybook/**/*", + "{workspaceRoot}/packages/frontend/components/src/**/*", "{workspaceRoot}/packages/frontend/core/src/**/*", "{workspaceRoot}/packages/common/infra/**/*", { diff --git a/tests/storybook/src/stories/page-list.stories.tsx b/tests/storybook/src/stories/page-list.stories.tsx index 0b904b1704..26c99d6478 100644 --- a/tests/storybook/src/stories/page-list.stories.tsx +++ b/tests/storybook/src/stories/page-list.stories.tsx @@ -11,7 +11,7 @@ import { PageListScrollContainer, PageTags, type PageTagsProps, -} from '@affine/component/page-list'; +} from '@affine/core/components/page-list'; import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models'; import { PageIcon, TagsIcon } from '@blocksuite/icons'; import { Schema, Workspace } from '@blocksuite/store'; diff --git a/yarn.lock b/yarn.lock index 5ee1c3df18..7902485314 100644 --- a/yarn.lock +++ b/yarn.lock @@ -327,8 +327,10 @@ __metadata: "@pmmmwh/react-refresh-webpack-plugin": "npm:^0.5.11" "@radix-ui/react-collapsible": "npm:^1.0.3" "@radix-ui/react-dialog": "npm:^1.0.4" + "@radix-ui/react-popover": "npm:^1.0.7" "@radix-ui/react-scroll-area": "npm:^1.0.5" "@radix-ui/react-select": "npm:^2.0.0" + "@radix-ui/react-toolbar": "npm:^1.0.4" "@react-hookz/web": "npm:^24.0.0" "@sentry/integrations": "npm:^7.83.0" "@sentry/react": "npm:^7.83.0" @@ -365,8 +367,10 @@ __metadata: jotai: "npm:^2.5.1" jotai-devtools: "npm:^0.7.0" jotai-effect: "npm:^0.2.3" + jotai-scope: "npm:^0.4.1" lit: "npm:^3.0.2" lodash-es: "npm:^4.17.21" + lottie-react: "npm:^2.4.0" lottie-web: "npm:^5.12.2" mime-types: "npm:^2.1.35" mini-css-extract-plugin: "npm:^2.7.6" @@ -380,6 +384,7 @@ __metadata: react-error-boundary: "npm:^4.0.11" react-is: "npm:18.2.0" react-router-dom: "npm:^6.16.0" + react-virtuoso: "npm:^4.6.2" rxjs: "npm:^7.8.1" ses: "npm:^1.0.0" source-map-loader: "npm:^4.0.1" From 9253e522aa7c04008b3a18a51ca0d988704d0877 Mon Sep 17 00:00:00 2001 From: liuyi Date: Thu, 11 Jan 2024 06:40:53 +0000 Subject: [PATCH 06/92] test(server): avoid progress get hold after tests finished (#5522) --- packages/backend/server/src/affine.config.ts | 38 +++++++----- packages/backend/server/src/affine.env.ts | 12 ++++ packages/backend/server/src/app.ts | 12 +++- packages/backend/server/src/cache/index.ts | 40 ++++++++----- .../backend/server/src/cache/instances.ts | 4 ++ .../backend/server/src/cache/interceptor.ts | 2 +- .../server/src/cache/{ => providers}/cache.ts | 6 +- .../server/src/cache/{ => providers}/redis.ts | 0 .../backend/server/src/cache/redis/index.ts | 38 ++++++++++++ packages/backend/server/src/config/def.ts | 4 ++ packages/backend/server/src/config/default.ts | 3 + packages/backend/server/src/config/index.ts | 2 - packages/backend/server/src/data/app.ts | 5 ++ .../server/src/data/commands/create.ts | 6 +- .../src/data/migrations/1698398506533-guid.ts | 4 +- .../1698652531198-user-features-init.ts | 7 ++- .../1699005339766-page-permission.ts | 7 +-- .../1702620653283-old-user-feature.ts | 8 +-- .../1703756315970-unamed-account.ts | 7 ++- .../1703828796699-workspace-blobs.ts | 6 +- .../1704352562369-refresh-user-features.ts | 7 ++- .../data/migrations/utils/user-features.ts | 7 +-- packages/backend/server/src/index.ts | 17 +++--- packages/backend/server/src/metrics/index.ts | 32 +++++++++- .../backend/server/src/metrics/metrics.ts | 2 + .../server/src/metrics/opentelemetry.ts | 28 +-------- .../server/src/modules/auth/resolver.ts | 7 ++- .../server/src/modules/sync/redis-adapter.ts | 14 +---- packages/backend/server/src/prelude.ts | 14 +---- packages/backend/server/src/session.ts | 47 +++------------ packages/backend/server/src/throttler.ts | 60 +++++++++++-------- packages/backend/server/tests/auth.spec.ts | 2 + packages/backend/server/tests/feature.spec.ts | 2 + packages/backend/server/tests/mailer.e2e.ts | 2 + packages/backend/server/tests/quota.spec.ts | 2 + packages/backend/server/tests/session.spec.ts | 2 + 36 files changed, 262 insertions(+), 194 deletions(-) create mode 100644 packages/backend/server/src/affine.env.ts create mode 100644 packages/backend/server/src/cache/instances.ts rename packages/backend/server/src/cache/{ => providers}/cache.ts (98%) rename packages/backend/server/src/cache/{ => providers}/redis.ts (100%) create mode 100644 packages/backend/server/src/cache/redis/index.ts diff --git a/packages/backend/server/src/affine.config.ts b/packages/backend/server/src/affine.config.ts index 43e9a83fac..869b20467d 100644 --- a/packages/backend/server/src/affine.config.ts +++ b/packages/backend/server/src/affine.config.ts @@ -5,21 +5,27 @@ const env = process.env; const node = AFFiNE.node; // TODO: may be separate config overring in `affine.[env].config`? -if (node.prod && env.R2_OBJECT_STORAGE_ACCOUNT_ID) { - AFFiNE.storage.providers.r2 = { - accountId: env.R2_OBJECT_STORAGE_ACCOUNT_ID, - credentials: { - accessKeyId: env.R2_OBJECT_STORAGE_ACCESS_KEY_ID!, - secretAccessKey: env.R2_OBJECT_STORAGE_SECRET_ACCESS_KEY!, - }, - }; - AFFiNE.storage.storages.avatar.provider = 'r2'; - AFFiNE.storage.storages.avatar.bucket = 'account-avatar'; - AFFiNE.storage.storages.avatar.publicLinkFactory = key => - `https://avatar.affineassets.com/${key}`; +if (node.prod) { + // Storage + if (env.R2_OBJECT_STORAGE_ACCOUNT_ID) { + AFFiNE.storage.providers.r2 = { + accountId: env.R2_OBJECT_STORAGE_ACCOUNT_ID, + credentials: { + accessKeyId: env.R2_OBJECT_STORAGE_ACCESS_KEY_ID!, + secretAccessKey: env.R2_OBJECT_STORAGE_SECRET_ACCESS_KEY!, + }, + }; + AFFiNE.storage.storages.avatar.provider = 'r2'; + AFFiNE.storage.storages.avatar.bucket = 'account-avatar'; + AFFiNE.storage.storages.avatar.publicLinkFactory = key => + `https://avatar.affineassets.com/${key}`; - AFFiNE.storage.storages.blob.provider = 'r2'; - AFFiNE.storage.storages.blob.bucket = `workspace-blobs-${ - AFFiNE.affine.canary ? 'canary' : 'prod' - }`; + AFFiNE.storage.storages.blob.provider = 'r2'; + AFFiNE.storage.storages.blob.bucket = `workspace-blobs-${ + AFFiNE.affine.canary ? 'canary' : 'prod' + }`; + } + + // Metrics + AFFiNE.metrics.enabled = true; } diff --git a/packages/backend/server/src/affine.env.ts b/packages/backend/server/src/affine.env.ts new file mode 100644 index 0000000000..5605964bf5 --- /dev/null +++ b/packages/backend/server/src/affine.env.ts @@ -0,0 +1,12 @@ +import { homedir } from 'node:os'; +import { join } from 'node:path'; + +import { config } from 'dotenv'; + +import { SERVER_FLAVOR } from './config/default'; + +if (SERVER_FLAVOR === 'selfhosted') { + config({ path: join(homedir(), '.affine', '.env') }); +} else { + config(); +} diff --git a/packages/backend/server/src/app.ts b/packages/backend/server/src/app.ts index ad97741aac..b123026318 100644 --- a/packages/backend/server/src/app.ts +++ b/packages/backend/server/src/app.ts @@ -3,24 +3,32 @@ import { APP_INTERCEPTOR } from '@nestjs/core'; import { AppController } from './app.controller'; import { CacheInterceptor, CacheModule } from './cache'; +import { RedisModule } from './cache/redis'; import { ConfigModule, SERVER_FLAVOR } from './config'; import { EventModule } from './event'; +import { MetricsModule } from './metrics'; import { BusinessModules } from './modules'; import { AuthModule } from './modules/auth'; import { PrismaModule } from './prisma'; import { SessionModule } from './session'; import { RateLimiterModule } from './throttler'; -const BasicModules = [ - PrismaModule, +export const BasicModules = [ ConfigModule.forRoot(), CacheModule, + PrismaModule, + MetricsModule, EventModule, SessionModule, RateLimiterModule, AuthModule, ]; +// better module registration logic +if (AFFiNE.redis.enabled) { + BasicModules.push(RedisModule); +} + @Module({ providers: [ { diff --git a/packages/backend/server/src/cache/index.ts b/packages/backend/server/src/cache/index.ts index 3a1dab5310..06c73367f2 100644 --- a/packages/backend/server/src/cache/index.ts +++ b/packages/backend/server/src/cache/index.ts @@ -1,26 +1,34 @@ -import { FactoryProvider, Global, Module } from '@nestjs/common'; +import { Global, Module, Provider, Type } from '@nestjs/common'; import { Redis } from 'ioredis'; -import { Config } from '../config'; -import { LocalCache } from './cache'; -import { RedisCache } from './redis'; +import { SessionCache, ThrottlerCache } from './instances'; +import { LocalCache } from './providers/cache'; +import { RedisCache } from './providers/redis'; +import { CacheRedis, SessionRedis, ThrottlerRedis } from './redis'; -const CacheProvider: FactoryProvider = { - provide: LocalCache, - useFactory: (config: Config) => { - return config.redis.enabled - ? new RedisCache(new Redis(config.redis)) - : new LocalCache(); - }, - inject: [Config], -}; +function makeCacheProvider(CacheToken: Type, RedisToken: Type): Provider { + return { + provide: CacheToken, + useFactory: (redis?: Redis) => { + return redis ? new RedisCache(redis) : new LocalCache(); + }, + inject: [{ token: RedisToken, optional: true }], + }; +} + +const CacheProvider = makeCacheProvider(LocalCache, CacheRedis); +const SessionCacheProvider = makeCacheProvider(SessionCache, SessionRedis); +const ThrottlerCacheProvider = makeCacheProvider( + ThrottlerCache, + ThrottlerRedis +); @Global() @Module({ - providers: [CacheProvider], - exports: [CacheProvider], + providers: [CacheProvider, SessionCacheProvider, ThrottlerCacheProvider], + exports: [CacheProvider, SessionCacheProvider, ThrottlerCacheProvider], }) export class CacheModule {} -export { LocalCache as Cache }; +export { LocalCache as Cache, SessionCache, ThrottlerCache }; export { CacheInterceptor, MakeCache, PreventCache } from './interceptor'; diff --git a/packages/backend/server/src/cache/instances.ts b/packages/backend/server/src/cache/instances.ts new file mode 100644 index 0000000000..0062aee8cc --- /dev/null +++ b/packages/backend/server/src/cache/instances.ts @@ -0,0 +1,4 @@ +import { LocalCache } from './providers/cache'; + +export class SessionCache extends LocalCache {} +export class ThrottlerCache extends LocalCache {} diff --git a/packages/backend/server/src/cache/interceptor.ts b/packages/backend/server/src/cache/interceptor.ts index 8cf078e18f..93cd388273 100644 --- a/packages/backend/server/src/cache/interceptor.ts +++ b/packages/backend/server/src/cache/interceptor.ts @@ -10,7 +10,7 @@ import { Reflector } from '@nestjs/core'; import { GqlContextType, GqlExecutionContext } from '@nestjs/graphql'; import { mergeMap, Observable, of } from 'rxjs'; -import { LocalCache } from './cache'; +import { LocalCache } from './providers/cache'; export const MakeCache = (key: string[], args?: string[]) => SetMetadata('cacheKey', [key, args]); diff --git a/packages/backend/server/src/cache/cache.ts b/packages/backend/server/src/cache/providers/cache.ts similarity index 98% rename from packages/backend/server/src/cache/cache.ts rename to packages/backend/server/src/cache/providers/cache.ts index 24df6ffe76..b770473bfb 100644 --- a/packages/backend/server/src/cache/cache.ts +++ b/packages/backend/server/src/cache/providers/cache.ts @@ -1,3 +1,4 @@ +import { Injectable } from '@nestjs/common'; import Keyv from 'keyv'; export interface CacheSetOptions { @@ -50,11 +51,12 @@ export interface Cache { mapLen(map: string): Promise; } +@Injectable() export class LocalCache implements Cache { private readonly kv: Keyv; - constructor() { - this.kv = new Keyv(); + constructor(opts: Keyv.Options = {}) { + this.kv = new Keyv(opts); } // standard operation diff --git a/packages/backend/server/src/cache/redis.ts b/packages/backend/server/src/cache/providers/redis.ts similarity index 100% rename from packages/backend/server/src/cache/redis.ts rename to packages/backend/server/src/cache/providers/redis.ts diff --git a/packages/backend/server/src/cache/redis/index.ts b/packages/backend/server/src/cache/redis/index.ts new file mode 100644 index 0000000000..d2807540c3 --- /dev/null +++ b/packages/backend/server/src/cache/redis/index.ts @@ -0,0 +1,38 @@ +import { Global, Injectable, Module, OnModuleDestroy } from '@nestjs/common'; +import { Redis as IORedis } from 'ioredis'; + +import { Config } from '../../config'; + +class Redis extends IORedis implements OnModuleDestroy { + onModuleDestroy() { + this.disconnect(); + } +} + +@Injectable() +export class CacheRedis extends Redis { + constructor(config: Config) { + super({ ...config.redis, db: config.redis.database }); + } +} + +@Injectable() +export class ThrottlerRedis extends Redis { + constructor(config: Config) { + super({ ...config.redis, db: config.redis.database + 1 }); + } +} + +@Injectable() +export class SessionRedis extends Redis { + constructor(config: Config) { + super({ ...config.redis, db: config.redis.database + 2 }); + } +} + +@Global() +@Module({ + providers: [CacheRedis, ThrottlerRedis, SessionRedis], + exports: [CacheRedis, ThrottlerRedis, SessionRedis], +}) +export class RedisModule {} diff --git a/packages/backend/server/src/config/def.ts b/packages/backend/server/src/config/def.ts index 34221ff3d9..ceb871ee1f 100644 --- a/packages/backend/server/src/config/def.ts +++ b/packages/backend/server/src/config/def.ts @@ -357,6 +357,10 @@ export interface AFFiNEConfig { }; }; + metrics: { + enabled: boolean; + }; + payment: { stripe: { keys: { diff --git a/packages/backend/server/src/config/default.ts b/packages/backend/server/src/config/default.ts index eca97146f2..94c8e72793 100644 --- a/packages/backend/server/src/config/default.ts +++ b/packages/backend/server/src/config/default.ts @@ -211,6 +211,9 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => { apiVersion: '2023-10-16', }, }, + metrics: { + enabled: false, + }, } satisfies AFFiNEConfig; applyEnvToConfig(defaultConfig); diff --git a/packages/backend/server/src/config/index.ts b/packages/backend/server/src/config/index.ts index 722dee3e93..433221764e 100644 --- a/packages/backend/server/src/config/index.ts +++ b/packages/backend/server/src/config/index.ts @@ -5,8 +5,6 @@ import { merge } from 'lodash-es'; import type { DeepPartial } from '../utils/types'; import type { AFFiNEConfig } from './def'; -import '../prelude'; - type ConstructorOf = { new (): T; }; diff --git a/packages/backend/server/src/data/app.ts b/packages/backend/server/src/data/app.ts index 5287e74fd9..5e00fd214a 100644 --- a/packages/backend/server/src/data/app.ts +++ b/packages/backend/server/src/data/app.ts @@ -1,3 +1,5 @@ +import '../prelude'; + import { Logger, Module } from '@nestjs/common'; import { CommandFactory } from 'nest-commander'; @@ -14,6 +16,9 @@ import { RevertCommand, RunCommand } from './commands/run'; enableUpdateAutoMerging: false, }, }, + metrics: { + enabled: false, + }, }), BusinessAppModule, ], diff --git a/packages/backend/server/src/data/commands/create.ts b/packages/backend/server/src/data/commands/create.ts index f8faf3a744..dba829aa2d 100644 --- a/packages/backend/server/src/data/commands/create.ts +++ b/packages/backend/server/src/data/commands/create.ts @@ -59,13 +59,13 @@ export class CreateCommand extends CommandRunner { } private createScript(name: string) { - const contents = ["import { PrismaService } from '../../prisma';", '']; + const contents = ["import { PrismaClient } from '@prisma/client';", '']; contents.push(`export class ${name} {`); contents.push(' // do the migration'); - contents.push(' static async up(db: PrismaService) {}'); + contents.push(' static async up(db: PrismaClient) {}'); contents.push(''); contents.push(' // revert the migration'); - contents.push(' static async down(db: PrismaService) {}'); + contents.push(' static async down(db: PrismaClient) {}'); contents.push('}'); diff --git a/packages/backend/server/src/data/migrations/1698398506533-guid.ts b/packages/backend/server/src/data/migrations/1698398506533-guid.ts index 4ace563ab1..e7e0262472 100644 --- a/packages/backend/server/src/data/migrations/1698398506533-guid.ts +++ b/packages/backend/server/src/data/migrations/1698398506533-guid.ts @@ -1,11 +1,11 @@ +import { PrismaClient } from '@prisma/client'; import { applyUpdate, Doc, encodeStateAsUpdate } from 'yjs'; -import { PrismaService } from '../../prisma'; import { DocID } from '../../utils/doc'; export class Guid1698398506533 { // do the migration - static async up(db: PrismaService) { + static async up(db: PrismaClient) { let turn = 0; let lastTurnCount = 100; while (lastTurnCount === 100) { diff --git a/packages/backend/server/src/data/migrations/1698652531198-user-features-init.ts b/packages/backend/server/src/data/migrations/1698652531198-user-features-init.ts index 786d467718..7d28a18d40 100644 --- a/packages/backend/server/src/data/migrations/1698652531198-user-features-init.ts +++ b/packages/backend/server/src/data/migrations/1698652531198-user-features-init.ts @@ -1,11 +1,12 @@ +import { PrismaClient } from '@prisma/client'; + import { Features } from '../../modules/features'; import { Quotas } from '../../modules/quota/schema'; -import { PrismaService } from '../../prisma'; import { migrateNewFeatureTable, upsertFeature } from './utils/user-features'; export class UserFeaturesInit1698652531198 { // do the migration - static async up(db: PrismaService) { + static async up(db: PrismaClient) { // upgrade features from lower version to higher version for (const feature of Features) { await upsertFeature(db, feature); @@ -18,7 +19,7 @@ export class UserFeaturesInit1698652531198 { } // revert the migration - static async down(_db: PrismaService) { + static async down(_db: PrismaClient) { // TODO: revert the migration } } diff --git a/packages/backend/server/src/data/migrations/1699005339766-page-permission.ts b/packages/backend/server/src/data/migrations/1699005339766-page-permission.ts index ae8c5620fd..ca3238b335 100644 --- a/packages/backend/server/src/data/migrations/1699005339766-page-permission.ts +++ b/packages/backend/server/src/data/migrations/1699005339766-page-permission.ts @@ -1,8 +1,7 @@ -import { PrismaService } from '../../prisma'; - +import { PrismaClient } from '@prisma/client'; export class PagePermission1699005339766 { // do the migration - static async up(db: PrismaService) { + static async up(db: PrismaClient) { let turn = 0; let lastTurnCount = 50; const done = new Set(); @@ -88,7 +87,7 @@ export class PagePermission1699005339766 { } // revert the migration - static async down(db: PrismaService) { + static async down(db: PrismaClient) { await db.workspaceUserPermission.deleteMany({}); await db.workspacePageUserPermission.deleteMany({}); } diff --git a/packages/backend/server/src/data/migrations/1702620653283-old-user-feature.ts b/packages/backend/server/src/data/migrations/1702620653283-old-user-feature.ts index 330b33f433..2656ab814c 100644 --- a/packages/backend/server/src/data/migrations/1702620653283-old-user-feature.ts +++ b/packages/backend/server/src/data/migrations/1702620653283-old-user-feature.ts @@ -1,9 +1,9 @@ -import { QuotaType } from '../../modules/quota/types'; -import { PrismaService } from '../../prisma'; +import { PrismaClient } from '@prisma/client'; +import { QuotaType } from '../../modules/quota/types'; export class OldUserFeature1702620653283 { // do the migration - static async up(db: PrismaService) { + static async up(db: PrismaClient) { await db.$transaction(async tx => { const latestFreePlan = await tx.features.findFirstOrThrow({ where: { feature: QuotaType.FreePlanV1 }, @@ -31,7 +31,7 @@ export class OldUserFeature1702620653283 { // revert the migration // WARN: this will drop all user features - static async down(db: PrismaService) { + static async down(db: PrismaClient) { await db.userFeatures.deleteMany({}); } } diff --git a/packages/backend/server/src/data/migrations/1703756315970-unamed-account.ts b/packages/backend/server/src/data/migrations/1703756315970-unamed-account.ts index bd8b7b8d4c..1a4bfae491 100644 --- a/packages/backend/server/src/data/migrations/1703756315970-unamed-account.ts +++ b/packages/backend/server/src/data/migrations/1703756315970-unamed-account.ts @@ -1,9 +1,10 @@ +import { PrismaClient } from '@prisma/client'; + import type { UserType } from '../../modules/users'; -import { PrismaService } from '../../prisma'; export class UnamedAccount1703756315970 { // do the migration - static async up(db: PrismaService) { + static async up(db: PrismaClient) { await db.$transaction(async tx => { // only find users with empty names const users = await db.$queryRaw< @@ -27,5 +28,5 @@ export class UnamedAccount1703756315970 { } // revert the migration - static async down(_db: PrismaService) {} + static async down(_db: PrismaClient) {} } diff --git a/packages/backend/server/src/data/migrations/1703828796699-workspace-blobs.ts b/packages/backend/server/src/data/migrations/1703828796699-workspace-blobs.ts index 6bbeff1c3e..0e505cc1db 100644 --- a/packages/backend/server/src/data/migrations/1703828796699-workspace-blobs.ts +++ b/packages/backend/server/src/data/migrations/1703828796699-workspace-blobs.ts @@ -1,11 +1,11 @@ import { ModuleRef } from '@nestjs/core'; +import { PrismaClient } from '@prisma/client'; import { WorkspaceBlobStorage } from '../../modules/storage'; -import { PrismaService } from '../../prisma'; export class WorkspaceBlobs1703828796699 { // do the migration - static async up(db: PrismaService, injector: ModuleRef) { + static async up(db: PrismaClient, injector: ModuleRef) { const blobStorage = injector.get(WorkspaceBlobStorage, { strict: false }); let hasMore = true; let turn = 0; @@ -32,7 +32,7 @@ export class WorkspaceBlobs1703828796699 { } // revert the migration - static async down(_db: PrismaService) { + static async down(_db: PrismaClient) { // old data kept, no need to downgrade the migration } } diff --git a/packages/backend/server/src/data/migrations/1704352562369-refresh-user-features.ts b/packages/backend/server/src/data/migrations/1704352562369-refresh-user-features.ts index 407aa22986..1e903344ae 100644 --- a/packages/backend/server/src/data/migrations/1704352562369-refresh-user-features.ts +++ b/packages/backend/server/src/data/migrations/1704352562369-refresh-user-features.ts @@ -1,10 +1,11 @@ +import { PrismaClient } from '@prisma/client'; + import { Features } from '../../modules/features'; -import { PrismaService } from '../../prisma'; import { upsertFeature } from './utils/user-features'; export class RefreshUserFeatures1704352562369 { // do the migration - static async up(db: PrismaService) { + static async up(db: PrismaClient) { // add early access v2 & copilot feature for (const feature of Features) { await upsertFeature(db, feature); @@ -12,5 +13,5 @@ export class RefreshUserFeatures1704352562369 { } // revert the migration - static async down(_db: PrismaService) {} + static async down(_db: PrismaClient) {} } diff --git a/packages/backend/server/src/data/migrations/utils/user-features.ts b/packages/backend/server/src/data/migrations/utils/user-features.ts index 0e93ce2328..3483420771 100644 --- a/packages/backend/server/src/data/migrations/utils/user-features.ts +++ b/packages/backend/server/src/data/migrations/utils/user-features.ts @@ -1,15 +1,14 @@ -import { Prisma } from '@prisma/client'; +import { Prisma, PrismaClient } from '@prisma/client'; import { CommonFeature, FeatureKind, FeatureType, } from '../../../modules/features'; -import { PrismaService } from '../../../prisma'; // upgrade features from lower version to higher version export async function upsertFeature( - db: PrismaService, + db: PrismaClient, feature: CommonFeature ): Promise { const hasEqualOrGreaterVersion = @@ -34,7 +33,7 @@ export async function upsertFeature( } } -export async function migrateNewFeatureTable(prisma: PrismaService) { +export async function migrateNewFeatureTable(prisma: PrismaClient) { const waitingList = await prisma.newFeaturesWaitingList.findMany(); for (const oldUser of waitingList) { const user = await prisma.user.findFirst({ diff --git a/packages/backend/server/src/index.ts b/packages/backend/server/src/index.ts index 187dd1f920..6333e1798d 100644 --- a/packages/backend/server/src/index.ts +++ b/packages/backend/server/src/index.ts @@ -1,6 +1,7 @@ /// -import { start as startAutoMetrics } from './metrics'; -startAutoMetrics(); +// keep the config import at the top +// eslint-disable-next-line simple-import-sort/imports +import './prelude'; import { NestFactory } from '@nestjs/core'; import type { NestExpressApplication } from '@nestjs/platform-express'; @@ -12,6 +13,7 @@ import { Config } from './config'; import { ExceptionLogger } from './middleware/exception-logger'; import { serverTimingAndCache } from './middleware/timing'; import { RedisIoAdapter } from './modules/sync/redis-adapter'; +import { CacheRedis } from './cache/redis'; const { NODE_ENV, AFFINE_ENV } = process.env; const app = await NestFactory.create(AppModule, { @@ -42,15 +44,10 @@ const config = app.get(Config); const host = config.node.prod ? '0.0.0.0' : 'localhost'; const port = config.port ?? 3010; -if (config.redis.enabled) { +if (!config.node.test && config.redis.enabled) { + const redis = app.get(CacheRedis, { strict: false }); const redisIoAdapter = new RedisIoAdapter(app); - await redisIoAdapter.connectToRedis( - config.redis.host, - config.redis.port, - config.redis.username, - config.redis.password, - config.redis.database - ); + await redisIoAdapter.connectToRedis(redis); app.useWebSocketAdapter(redisIoAdapter); } diff --git a/packages/backend/server/src/metrics/index.ts b/packages/backend/server/src/metrics/index.ts index 655c717c3e..9ca470b582 100644 --- a/packages/backend/server/src/metrics/index.ts +++ b/packages/backend/server/src/metrics/index.ts @@ -1,3 +1,33 @@ +import { Global, Module, OnModuleDestroy, OnModuleInit } from '@nestjs/common'; +import { NodeSDK } from '@opentelemetry/sdk-node'; + +import { Config } from '../config'; +import { parseEnvValue } from '../config/def'; +import { createSDK, registerCustomMetrics } from './opentelemetry'; + +@Global() +@Module({}) +export class MetricsModule implements OnModuleInit, OnModuleDestroy { + private sdk: NodeSDK | null = null; + constructor(private readonly config: Config) {} + + onModuleInit() { + if ( + this.config.metrics.enabled && + !parseEnvValue(process.env.DISABLE_TELEMETRY, 'boolean') + ) { + this.sdk = createSDK(); + this.sdk.start(); + registerCustomMetrics(); + } + } + + async onModuleDestroy() { + if (this.config.metrics.enabled && this.sdk) { + await this.sdk.shutdown(); + } + } +} + export * from './metrics'; -export { start } from './opentelemetry'; export * from './utils'; diff --git a/packages/backend/server/src/metrics/metrics.ts b/packages/backend/server/src/metrics/metrics.ts index ccbb7be606..83e6d72216 100644 --- a/packages/backend/server/src/metrics/metrics.ts +++ b/packages/backend/server/src/metrics/metrics.ts @@ -127,3 +127,5 @@ export const metrics = new Proxy>( }, } ); + +export function stopMetrics() {} diff --git a/packages/backend/server/src/metrics/opentelemetry.ts b/packages/backend/server/src/metrics/opentelemetry.ts index 54e449ae13..0f3206bb2c 100644 --- a/packages/backend/server/src/metrics/opentelemetry.ts +++ b/packages/backend/server/src/metrics/opentelemetry.ts @@ -33,7 +33,6 @@ import prismaInstrument from '@prisma/instrumentation'; const { PrismaInstrumentation } = prismaInstrument; -import { parseEnvValue } from '../config/def'; import { PrismaMetricProducer } from './prisma'; abstract class OpentelemetryFactor { @@ -116,7 +115,8 @@ class DebugOpentelemetryFactor extends OpentelemetryFactor { } } -function createSDK() { +// TODO(@forehalo): make it configurable +export function createSDK() { let factor: OpentelemetryFactor | null = null; if (process.env.NODE_ENV === 'production') { factor = new GCloudOpentelemetryFactor(); @@ -129,21 +129,11 @@ function createSDK() { return factor?.create(); } -let OPENTELEMETRY_STARTED = false; - -function ensureStarted() { - if (!OPENTELEMETRY_STARTED) { - OPENTELEMETRY_STARTED = true; - start(); - } -} - function getMeterProvider() { - ensureStarted(); return metrics.getMeterProvider(); } -function registerCustomMetrics() { +export function registerCustomMetrics() { const hostMetricsMonitoring = new HostMetrics({ name: 'instance-host-metrics', meterProvider: getMeterProvider() as MeterProvider, @@ -154,15 +144,3 @@ function registerCustomMetrics() { export function getMeter(name = 'business') { return getMeterProvider().getMeter(name); } - -export function start() { - if (parseEnvValue(process.env.DISABLE_TELEMETRY, 'boolean')) { - return; - } - const sdk = createSDK(); - - if (sdk) { - sdk.start(); - registerCustomMetrics(); - } -} diff --git a/packages/backend/server/src/modules/auth/resolver.ts b/packages/backend/server/src/modules/auth/resolver.ts index 697812520f..aac7b4e673 100644 --- a/packages/backend/server/src/modules/auth/resolver.ts +++ b/packages/backend/server/src/modules/auth/resolver.ts @@ -167,8 +167,13 @@ export class AuthResolver { @CurrentUser() user: UserType, @Args('token') token: string ) { + const key = await this.session.get(token); + if (!key) { + throw new ForbiddenException('Invalid token'); + } + // email has set token in `sendVerifyChangeEmail` - const [id, email] = (await this.session.get(token)).split(','); + const [id, email] = key.split(','); if (!id || id !== user.id || !email) { throw new ForbiddenException('Invalid token'); } diff --git a/packages/backend/server/src/modules/sync/redis-adapter.ts b/packages/backend/server/src/modules/sync/redis-adapter.ts index 312a0c1d7e..46d1591975 100644 --- a/packages/backend/server/src/modules/sync/redis-adapter.ts +++ b/packages/backend/server/src/modules/sync/redis-adapter.ts @@ -6,18 +6,8 @@ import { ServerOptions } from 'socket.io'; export class RedisIoAdapter extends IoAdapter { private adapterConstructor: ReturnType | undefined; - async connectToRedis( - host: string, - port: number, - username: string, - password: string, - db: number - ): Promise { - const pubClient = new Redis(port, host, { - username, - password, - db, - }); + async connectToRedis(redis: Redis): Promise { + const pubClient = redis; pubClient.on('error', err => { console.error(err); }); diff --git a/packages/backend/server/src/prelude.ts b/packages/backend/server/src/prelude.ts index 2ca5041763..d70e339c2c 100644 --- a/packages/backend/server/src/prelude.ts +++ b/packages/backend/server/src/prelude.ts @@ -1,17 +1,5 @@ -import { homedir } from 'node:os'; -import { join } from 'node:path'; - -import { config } from 'dotenv'; - -import { SERVER_FLAVOR } from './config/default'; - -if (SERVER_FLAVOR === 'selfhosted') { - config({ path: join(homedir(), '.affine', '.env') }); -} else { - config(); -} - import 'reflect-metadata'; +import './affine.env'; import './affine'; import './affine.config'; diff --git a/packages/backend/server/src/session.ts b/packages/backend/server/src/session.ts index e3ca61c4f6..878bf0e821 100644 --- a/packages/backend/server/src/session.ts +++ b/packages/backend/server/src/session.ts @@ -1,44 +1,13 @@ -import KeyvRedis from '@keyv/redis'; -import { - FactoryProvider, - Global, - Inject, - Injectable, - Module, -} from '@nestjs/common'; -import Redis from 'ioredis'; -import Keyv from 'keyv'; +import { Global, Injectable, Module } from '@nestjs/common'; -import { Config } from './config'; - -export const KeyvProvide = Symbol('KeyvProvide'); - -export const KeyvProvider: FactoryProvider = { - provide: KeyvProvide, - useFactory(config: Config) { - if (config.redis.enabled) { - return new Keyv({ - store: new KeyvRedis( - new Redis(config.redis.port, config.redis.host, { - username: config.redis.username, - password: config.redis.password, - db: config.redis.database + 2, - }) - ), - }); - } else { - return new Keyv(); - } - }, - inject: [Config], -}; +import { SessionCache } from './cache/instances'; @Injectable() export class SessionService { private readonly prefix = 'session:'; private readonly sessionTtl = 30 * 60 * 1000; // 30 min - constructor(@Inject(KeyvProvide) private readonly cache: Keyv) {} + constructor(private readonly cache: SessionCache) {} /** * get session @@ -46,7 +15,7 @@ export class SessionService { * @returns */ async get(key: string) { - return this.cache.get(this.prefix + key); + return this.cache.get(this.prefix + key); } /** @@ -57,7 +26,9 @@ export class SessionService { * @returns return true if success */ async set(key: string, value?: any, sessionTtl = this.sessionTtl) { - return this.cache.set(this.prefix + key, value, sessionTtl); + return this.cache.set(this.prefix + key, value, { + ttl: sessionTtl, + }); } async delete(key: string) { @@ -67,7 +38,7 @@ export class SessionService { @Global() @Module({ - providers: [KeyvProvider, SessionService], - exports: [KeyvProvider, SessionService], + providers: [SessionService], + exports: [SessionService], }) export class SessionModule {} diff --git a/packages/backend/server/src/throttler.ts b/packages/backend/server/src/throttler.ts index 76cf0e5720..a94fe9355b 100644 --- a/packages/backend/server/src/throttler.ts +++ b/packages/backend/server/src/throttler.ts @@ -5,42 +5,50 @@ import { ThrottlerGuard, ThrottlerModule, ThrottlerModuleOptions, + ThrottlerOptionsFactory, } from '@nestjs/throttler'; -import Redis from 'ioredis'; import { ThrottlerStorageRedisService } from 'nestjs-throttler-storage-redis'; +import { ThrottlerCache } from './cache'; import { Config } from './config'; import { getRequestResponseFromContext } from './utils/nestjs'; +@Injectable() +class CustomOptionsFactory implements ThrottlerOptionsFactory { + constructor( + private readonly config: Config, + private readonly cache: ThrottlerCache + ) {} + createThrottlerOptions() { + const options: ThrottlerModuleOptions = { + throttlers: [ + { + ttl: this.config.rateLimiter.ttl, + limit: this.config.rateLimiter.limit, + }, + ], + skipIf: () => { + return !this.config.node.prod || this.config.affine.canary; + }, + }; + + if (this.config.redis.enabled) { + new Logger(RateLimiterModule.name).log('Use Redis'); + options.storage = new ThrottlerStorageRedisService( + // @ts-expect-error hidden field + this.cache.redis + ); + } + + return options; + } +} + @Global() @Module({ imports: [ ThrottlerModule.forRootAsync({ - inject: [Config], - useFactory: (config: Config): ThrottlerModuleOptions => { - const options: ThrottlerModuleOptions = { - throttlers: [ - { - ttl: config.rateLimiter.ttl, - limit: config.rateLimiter.limit, - }, - ], - skipIf: () => { - return !config.node.prod || config.affine.canary; - }, - }; - if (config.redis.enabled) { - new Logger(RateLimiterModule.name).log('Use Redis'); - options.storage = new ThrottlerStorageRedisService( - new Redis(config.redis.port, config.redis.host, { - username: config.redis.username, - password: config.redis.password, - db: config.redis.database + 1, - }) - ); - } - return options; - }, + useClass: CustomOptionsFactory, }), ], }) diff --git a/packages/backend/server/tests/auth.spec.ts b/packages/backend/server/tests/auth.spec.ts index b3edd260f2..e87125b796 100644 --- a/packages/backend/server/tests/auth.spec.ts +++ b/packages/backend/server/tests/auth.spec.ts @@ -3,6 +3,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { PrismaClient } from '@prisma/client'; import test from 'ava'; +import { CacheModule } from '../src/cache'; import { ConfigModule } from '../src/config'; import { RevertCommand, RunCommand } from '../src/data/commands/run'; import { GqlModule } from '../src/graphql.module'; @@ -39,6 +40,7 @@ test.beforeEach(async () => { https: true, }), PrismaModule, + CacheModule, GqlModule, AuthModule, RateLimiterModule, diff --git a/packages/backend/server/tests/feature.spec.ts b/packages/backend/server/tests/feature.spec.ts index 50896d21d1..320f970898 100644 --- a/packages/backend/server/tests/feature.spec.ts +++ b/packages/backend/server/tests/feature.spec.ts @@ -5,6 +5,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { PrismaClient } from '@prisma/client'; import ava, { type TestFn } from 'ava'; +import { CacheModule } from '../src/cache'; import { ConfigModule } from '../src/config'; import { RevertCommand, RunCommand } from '../src/data/commands/run'; import { AuthModule } from '../src/modules/auth'; @@ -80,6 +81,7 @@ test.beforeEach(async t => { }, }), PrismaModule, + CacheModule, AuthModule, FeatureModule, RateLimiterModule, diff --git a/packages/backend/server/tests/mailer.e2e.ts b/packages/backend/server/tests/mailer.e2e.ts index ea8beea45f..3538d4e303 100644 --- a/packages/backend/server/tests/mailer.e2e.ts +++ b/packages/backend/server/tests/mailer.e2e.ts @@ -10,6 +10,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { PrismaClient } from '@prisma/client'; import ava, { type TestFn } from 'ava'; +import { CacheModule } from '../src/cache'; import { ConfigModule } from '../src/config'; import { RevertCommand, RunCommand } from '../src/data/commands/run'; import { GqlModule } from '../src/graphql.module'; @@ -45,6 +46,7 @@ test.beforeEach(async t => { PrismaModule, GqlModule, AuthModule, + CacheModule, RateLimiterModule, ], providers: [RevertCommand, RunCommand], diff --git a/packages/backend/server/tests/quota.spec.ts b/packages/backend/server/tests/quota.spec.ts index d819c21752..5e5237759a 100644 --- a/packages/backend/server/tests/quota.spec.ts +++ b/packages/backend/server/tests/quota.spec.ts @@ -4,6 +4,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { PrismaClient } from '@prisma/client'; import ava, { type TestFn } from 'ava'; +import { CacheModule } from '../src/cache'; import { ConfigModule } from '../src/config'; import { RevertCommand, RunCommand } from '../src/data/commands/run'; import { EventModule } from '../src/event'; @@ -49,6 +50,7 @@ test.beforeEach(async t => { https: true, }), PrismaModule, + CacheModule, AuthModule, EventModule, QuotaModule, diff --git a/packages/backend/server/tests/session.spec.ts b/packages/backend/server/tests/session.spec.ts index 18d680e87d..71b08a485a 100644 --- a/packages/backend/server/tests/session.spec.ts +++ b/packages/backend/server/tests/session.spec.ts @@ -3,6 +3,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import ava, { type TestFn } from 'ava'; +import { CacheModule } from '../src/cache'; import { ConfigModule } from '../src/config'; import { SessionModule, SessionService } from '../src/session'; @@ -19,6 +20,7 @@ test.beforeEach(async t => { enabled: false, }, }), + CacheModule, SessionModule, ], }).compile(); From 12fdb18a807c97f3fe70e1224c9d1c8a0edd4dca Mon Sep 17 00:00:00 2001 From: liuyi Date: Thu, 11 Jan 2024 06:40:55 +0000 Subject: [PATCH 07/92] test(server): make server testing utils (#5544) --- packages/backend/server/src/app.ts | 8 +- packages/backend/server/src/graphql.module.ts | 11 +- .../backend/server/src/modules/quota/index.ts | 1 + .../src/modules/storage/__tests__/fs.spec.ts | 2 +- packages/backend/server/src/prisma/index.ts | 13 +- packages/backend/server/tests/app.e2e.ts | 30 ++-- packages/backend/server/tests/auth.e2e.ts | 42 +----- packages/backend/server/tests/auth.spec.ts | 34 +---- packages/backend/server/tests/doc.spec.ts | 54 ++----- packages/backend/server/tests/feature.spec.ts | 54 ++----- packages/backend/server/tests/history.spec.ts | 31 +--- packages/backend/server/tests/mailer.e2e.ts | 23 +-- packages/backend/server/tests/mailer.spec.ts | 42 +++--- packages/backend/server/tests/quota.spec.ts | 54 +------ packages/backend/server/tests/session.spec.ts | 13 +- packages/backend/server/tests/user.e2e.ts | 31 +--- packages/backend/server/tests/utils/utils.ts | 138 +++++++++++++----- .../server/tests/workspace-blobs.spec.ts | 37 +---- .../server/tests/workspace-invite.e2e.ts | 39 +---- .../backend/server/tests/workspace.e2e.ts | 32 +--- 20 files changed, 238 insertions(+), 451 deletions(-) diff --git a/packages/backend/server/src/app.ts b/packages/backend/server/src/app.ts index b123026318..ea47c41a32 100644 --- a/packages/backend/server/src/app.ts +++ b/packages/backend/server/src/app.ts @@ -1,4 +1,4 @@ -import { Module } from '@nestjs/common'; +import { DynamicModule, Module, Type } from '@nestjs/common'; import { APP_INTERCEPTOR } from '@nestjs/core'; import { AppController } from './app.controller'; @@ -13,7 +13,7 @@ import { PrismaModule } from './prisma'; import { SessionModule } from './session'; import { RateLimiterModule } from './throttler'; -export const BasicModules = [ +export const FunctionalityModules: Array = [ ConfigModule.forRoot(), CacheModule, PrismaModule, @@ -26,7 +26,7 @@ export const BasicModules = [ // better module registration logic if (AFFiNE.redis.enabled) { - BasicModules.push(RedisModule); + FunctionalityModules.push(RedisModule); } @Module({ @@ -36,7 +36,7 @@ if (AFFiNE.redis.enabled) { useClass: CacheInterceptor, }, ], - imports: [...BasicModules, ...BusinessModules], + imports: [...FunctionalityModules, ...BusinessModules], controllers: SERVER_FLAVOR === 'selfhosted' ? [] : [AppController], }) export class AppModule {} diff --git a/packages/backend/server/src/graphql.module.ts b/packages/backend/server/src/graphql.module.ts index 38ded5c1df..71610e2235 100644 --- a/packages/backend/server/src/graphql.module.ts +++ b/packages/backend/server/src/graphql.module.ts @@ -21,11 +21,12 @@ import { GQLLoggerPlugin } from './graphql/logger-plugin'; csrfPrevention: { requestHeaders: ['content-type'], }, - autoSchemaFile: join( - fileURLToPath(import.meta.url), - '..', - 'schema.gql' - ), + autoSchemaFile: config.node.test + ? join( + fileURLToPath(import.meta.url), + '../../node_modules/.cache/schema.gql' + ) + : join(fileURLToPath(import.meta.url), '..', 'schema.gql'), context: ({ req, res }: { req: Request; res: Response }) => ({ req, res, diff --git a/packages/backend/server/src/modules/quota/index.ts b/packages/backend/server/src/modules/quota/index.ts index 2fc8c8edd5..224422fa17 100644 --- a/packages/backend/server/src/modules/quota/index.ts +++ b/packages/backend/server/src/modules/quota/index.ts @@ -12,6 +12,7 @@ import { QuotaManagementService } from './storage'; * - quota statistics */ @Module({ + // FIXME: Quota really need to know `Storage`? imports: [StorageModule], providers: [PermissionService, QuotaService, QuotaManagementService], exports: [QuotaService, QuotaManagementService], diff --git a/packages/backend/server/src/modules/storage/__tests__/fs.spec.ts b/packages/backend/server/src/modules/storage/__tests__/fs.spec.ts index 242c7d5664..3f1f36b874 100644 --- a/packages/backend/server/src/modules/storage/__tests__/fs.spec.ts +++ b/packages/backend/server/src/modules/storage/__tests__/fs.spec.ts @@ -101,7 +101,7 @@ test('list recursively', async t => { t.is(r5.length, 20); }); -test.only('delete', async t => { +test('delete', async t => { const provider = createProvider(); const key = 'testKey'; const body = Buffer.from('testBody'); diff --git a/packages/backend/server/src/prisma/index.ts b/packages/backend/server/src/prisma/index.ts index b3f3b330d8..1c5c8e257b 100644 --- a/packages/backend/server/src/prisma/index.ts +++ b/packages/backend/server/src/prisma/index.ts @@ -1,11 +1,18 @@ -import { Global, Module } from '@nestjs/common'; +import { Global, Module, Provider } from '@nestjs/common'; +import { PrismaClient } from '@prisma/client'; import { PrismaService } from './service'; +// both `PrismaService` and `PrismaClient` can be injected +const clientProvider: Provider = { + provide: PrismaClient, + useExisting: PrismaService, +}; + @Global() @Module({ - providers: [PrismaService], - exports: [PrismaService], + providers: [PrismaService, clientProvider], + exports: [PrismaService, clientProvider], }) export class PrismaModule {} export { PrismaService } from './service'; diff --git a/packages/backend/server/tests/app.e2e.ts b/packages/backend/server/tests/app.e2e.ts index 175f58b597..600737252e 100644 --- a/packages/backend/server/tests/app.e2e.ts +++ b/packages/backend/server/tests/app.e2e.ts @@ -3,17 +3,16 @@ import { randomUUID } from 'node:crypto'; import { Transformer } from '@napi-rs/image'; import type { INestApplication } from '@nestjs/common'; -import { Test } from '@nestjs/testing'; import { hashSync } from '@node-rs/argon2'; import { type User } from '@prisma/client'; import ava, { type TestFn } from 'ava'; import type { Express } from 'express'; -import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs'; import request from 'supertest'; import { AppModule } from '../src/app'; import { FeatureManagementService } from '../src/modules/features'; import { PrismaService } from '../src/prisma/service'; +import { createTestingApp } from './utils'; const gql = '/graphql'; @@ -49,25 +48,18 @@ class FakePrisma { } test.beforeEach(async t => { - const module = await Test.createTestingModule({ + const { app } = await createTestingApp({ imports: [AppModule], - }) - .overrideProvider(PrismaService) - .useClass(FakePrisma) - .overrideProvider(FeatureManagementService) - .useValue({ canEarlyAccess: () => true }) - .compile(); - t.context.app = module.createNestApplication({ - cors: true, - bodyParser: true, + tapModule(builder) { + builder + .overrideProvider(PrismaService) + .useClass(FakePrisma) + .overrideProvider(FeatureManagementService) + .useValue({ canEarlyAccess: () => true }); + }, }); - t.context.app.use( - graphqlUploadExpress({ - maxFileSize: 10 * 1024 * 1024, - maxFiles: 5, - }) - ); - await t.context.app.init(); + + t.context.app = app; }); test.afterEach.always(async t => { diff --git a/packages/backend/server/tests/auth.e2e.ts b/packages/backend/server/tests/auth.e2e.ts index a159a8ae6b..440c189ca6 100644 --- a/packages/backend/server/tests/auth.e2e.ts +++ b/packages/backend/server/tests/auth.e2e.ts @@ -3,19 +3,13 @@ import { getLatestMailMessage, } from '@affine-test/kit/utils/cloud'; import type { INestApplication } from '@nestjs/common'; -import { Test } from '@nestjs/testing'; -import { PrismaClient } from '@prisma/client'; import ava, { type TestFn } from 'ava'; -import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs'; -import { AppModule } from '../src/app'; -import { RevertCommand, RunCommand } from '../src/data/commands/run'; import { MailService } from '../src/modules/auth/mailer'; import { AuthService } from '../src/modules/auth/service'; import { changeEmail, - createWorkspace, - initFeatureConfigs, + createTestingApp, sendChangeEmail, sendVerifyChangeEmail, signUp, @@ -23,44 +17,20 @@ import { const test = ava as TestFn<{ app: INestApplication; - client: PrismaClient; auth: AuthService; mail: MailService; }>; test.beforeEach(async t => { - const client = new PrismaClient(); - t.context.client = client; - await client.$connect(); - await client.user.deleteMany({}); - await client.snapshot.deleteMany({}); - await client.update.deleteMany({}); - await client.workspace.deleteMany({}); - await client.$disconnect(); - const module = await Test.createTestingModule({ - imports: [AppModule], - providers: [RevertCommand, RunCommand], - }).compile(); - const app = module.createNestApplication(); - app.use( - graphqlUploadExpress({ - maxFileSize: 10 * 1024 * 1024, - maxFiles: 5, - }) - ); - await app.init(); - - const auth = module.get(AuthService); - const mail = module.get(MailService); + const { app } = await createTestingApp(); + const auth = app.get(AuthService); + const mail = app.get(MailService); t.context.app = app; t.context.auth = auth; t.context.mail = mail; - - // init features - await initFeatureConfigs(module); }); -test.afterEach(async t => { +test.afterEach.always(async t => { await t.context.app.close(); }); @@ -73,8 +43,6 @@ test('change email', async t => { const u1 = await signUp(app, 'u1', u1Email, '1'); - await createWorkspace(app, u1.token.token); - const primitiveMailCount = await getCurrentMailMessageCount(); await sendChangeEmail(app, u1.token.token, u1Email, 'affine.pro'); diff --git a/packages/backend/server/tests/auth.spec.ts b/packages/backend/server/tests/auth.spec.ts index e87125b796..906182ea2a 100644 --- a/packages/backend/server/tests/auth.spec.ts +++ b/packages/backend/server/tests/auth.spec.ts @@ -1,34 +1,19 @@ /// -import { Test, TestingModule } from '@nestjs/testing'; -import { PrismaClient } from '@prisma/client'; +import { TestingModule } from '@nestjs/testing'; import test from 'ava'; -import { CacheModule } from '../src/cache'; import { ConfigModule } from '../src/config'; -import { RevertCommand, RunCommand } from '../src/data/commands/run'; -import { GqlModule } from '../src/graphql.module'; -import { AuthModule } from '../src/modules/auth'; import { AuthResolver } from '../src/modules/auth/resolver'; import { AuthService } from '../src/modules/auth/service'; -import { PrismaModule } from '../src/prisma'; import { mintChallengeResponse, verifyChallengeResponse } from '../src/storage'; -import { RateLimiterModule } from '../src/throttler'; -import { initFeatureConfigs } from './utils'; +import { createTestingModule } from './utils'; let authService: AuthService; let authResolver: AuthResolver; let module: TestingModule; -// cleanup database before each test test.beforeEach(async () => { - const client = new PrismaClient(); - await client.$connect(); - await client.user.deleteMany({}); - await client.$disconnect(); -}); - -test.beforeEach(async () => { - module = await Test.createTestingModule({ + module = await createTestingModule({ imports: [ ConfigModule.forRoot({ auth: { @@ -39,20 +24,11 @@ test.beforeEach(async () => { host: 'example.org', https: true, }), - PrismaModule, - CacheModule, - GqlModule, - AuthModule, - RateLimiterModule, - RevertCommand, - RunCommand, ], - }).compile(); + }); + authService = module.get(AuthService); authResolver = module.get(AuthResolver); - - // init features - await initFeatureConfigs(module); }); test.afterEach.always(async () => { diff --git a/packages/backend/server/tests/doc.spec.ts b/packages/backend/server/tests/doc.spec.ts index e004d697ec..d772514c24 100644 --- a/packages/backend/server/tests/doc.spec.ts +++ b/packages/backend/server/tests/doc.spec.ts @@ -1,9 +1,7 @@ import { mock } from 'node:test'; -import type { INestApplication } from '@nestjs/common'; -import { Test, TestingModule } from '@nestjs/testing'; +import { TestingModule } from '@nestjs/testing'; import test from 'ava'; -import { register } from 'prom-client'; import * as Sinon from 'sinon'; import { applyUpdate, @@ -12,37 +10,19 @@ import { encodeStateAsUpdate, } from 'yjs'; -import { CacheModule } from '../src/cache'; -import { Config, ConfigModule } from '../src/config'; -import { - collectMigrations, - RevertCommand, - RunCommand, -} from '../src/data/commands/run'; -import { EventModule } from '../src/event'; +import { Config } from '../src/config'; import { DocManager, DocModule } from '../src/modules/doc'; import { QuotaModule } from '../src/modules/quota'; import { StorageModule } from '../src/modules/storage'; -import { PrismaModule, PrismaService } from '../src/prisma'; -import { flushDB } from './utils'; +import { PrismaService } from '../src/prisma'; +import { createTestingModule, initTestingDB } from './utils'; const createModule = () => { - return Test.createTestingModule({ - imports: [ - PrismaModule, - CacheModule, - EventModule, - QuotaModule, - StorageModule, - ConfigModule.forRoot(), - DocModule, - RevertCommand, - RunCommand, - ], - }).compile(); + return createTestingModule({ + imports: [QuotaModule, StorageModule, DocModule], + }); }; -let app: INestApplication; let m: TestingModule; let timer: Sinon.SinonFakeTimers; @@ -51,44 +31,34 @@ test.beforeEach(async () => { timer = Sinon.useFakeTimers({ toFake: ['setInterval'], }); - await flushDB(); m = await createModule(); - app = m.createNestApplication(); - app.enableShutdownHooks(); - await app.init(); - - // init features - const run = m.get(RunCommand); - const revert = m.get(RevertCommand); - const migrations = await collectMigrations(); - await Promise.allSettled(migrations.map(m => revert.run([m.name]))); - await run.run(); + await m.init(); + await initTestingDB(m.get(PrismaService)); }); test.afterEach.always(async () => { - await app.close(); await m.close(); timer.restore(); }); test('should setup update poll interval', async t => { - register.clear(); const m = await createModule(); const manager = m.get(DocManager); const fake = mock.method(manager, 'setup'); - await m.createNestApplication().init(); + await m.init(); t.is(fake.mock.callCount(), 1); // @ts-expect-error private member t.truthy(manager.job); + m.close(); }); test('should be able to stop poll', async t => { const manager = m.get(DocManager); const fake = mock.method(manager, 'destroy'); - await app.close(); + await m.close(); t.is(fake.mock.callCount(), 1); // @ts-expect-error private member diff --git a/packages/backend/server/tests/feature.spec.ts b/packages/backend/server/tests/feature.spec.ts index 320f970898..20012f7806 100644 --- a/packages/backend/server/tests/feature.spec.ts +++ b/packages/backend/server/tests/feature.spec.ts @@ -1,14 +1,9 @@ /// -import { Injectable } from '@nestjs/common'; -import { Test, TestingModule } from '@nestjs/testing'; -import { PrismaClient } from '@prisma/client'; +import { INestApplication, Injectable } from '@nestjs/common'; import ava, { type TestFn } from 'ava'; -import { CacheModule } from '../src/cache'; import { ConfigModule } from '../src/config'; -import { RevertCommand, RunCommand } from '../src/data/commands/run'; -import { AuthModule } from '../src/modules/auth'; import { AuthService } from '../src/modules/auth/service'; import { FeatureManagementService, @@ -19,9 +14,8 @@ import { import { UserType } from '../src/modules/users/types'; import { WorkspaceResolver } from '../src/modules/workspaces/resolvers'; import { Permission } from '../src/modules/workspaces/types'; -import { PrismaModule, PrismaService } from '../src/prisma'; -import { RateLimiterModule } from '../src/throttler'; -import { initFeatureConfigs } from './utils'; +import { PrismaService } from '../src/prisma'; +import { createTestingApp } from './utils'; @Injectable() class WorkspaceResolverMock { @@ -53,20 +47,11 @@ const test = ava as TestFn<{ feature: FeatureService; workspace: WorkspaceResolver; management: FeatureManagementService; - app: TestingModule; + app: INestApplication; }>; -// cleanup database before each test -test.beforeEach(async () => { - const client = new PrismaClient(); - await client.$connect(); - await client.user.deleteMany({}); - await client.workspace.deleteMany({}); - await client.$disconnect(); -}); - test.beforeEach(async t => { - const module = await Test.createTestingModule({ + const { app } = await createTestingApp({ imports: [ ConfigModule.forRoot({ auth: { @@ -80,28 +65,21 @@ test.beforeEach(async t => { earlyAccessPreview: true, }, }), - PrismaModule, - CacheModule, - AuthModule, FeatureModule, - RateLimiterModule, - RevertCommand, - RunCommand, ], providers: [WorkspaceResolver], - }) - .overrideProvider(WorkspaceResolver) - .useClass(WorkspaceResolverMock) - .compile(); + tapModule: module => { + module + .overrideProvider(WorkspaceResolver) + .useClass(WorkspaceResolverMock); + }, + }); - t.context.app = module; - t.context.auth = module.get(AuthService); - t.context.feature = module.get(FeatureService); - t.context.workspace = module.get(WorkspaceResolver); - t.context.management = module.get(FeatureManagementService); - - // init features - await initFeatureConfigs(module); + t.context.app = app; + t.context.auth = app.get(AuthService); + t.context.feature = app.get(FeatureService); + t.context.workspace = app.get(WorkspaceResolver); + t.context.management = app.get(FeatureManagementService); }); test.afterEach.always(async t => { diff --git a/packages/backend/server/tests/history.spec.ts b/packages/backend/server/tests/history.spec.ts index 050f30de56..7d416bd08d 100644 --- a/packages/backend/server/tests/history.spec.ts +++ b/packages/backend/server/tests/history.spec.ts @@ -1,40 +1,26 @@ -import { INestApplication } from '@nestjs/common'; -import { ScheduleModule } from '@nestjs/schedule'; -import { Test, TestingModule } from '@nestjs/testing'; +import { TestingModule } from '@nestjs/testing'; import type { Snapshot } from '@prisma/client'; import test from 'ava'; import * as Sinon from 'sinon'; -import { ConfigModule } from '../src/config'; -import { EventModule, type EventPayload } from '../src/event'; +import { type EventPayload } from '../src/event'; import { DocHistoryManager } from '../src/modules/doc'; import { QuotaModule } from '../src/modules/quota'; import { StorageModule } from '../src/modules/storage'; -import { PrismaModule, PrismaService } from '../src/prisma'; -import { flushDB } from './utils'; +import { PrismaService } from '../src/prisma'; +import { createTestingModule } from './utils'; -let app: INestApplication; let m: TestingModule; let manager: DocHistoryManager; let db: PrismaService; // cleanup database before each test test.beforeEach(async () => { - await flushDB(); - m = await Test.createTestingModule({ - imports: [ - PrismaModule, - QuotaModule, - EventModule, - StorageModule, - ScheduleModule.forRoot(), - ConfigModule.forRoot(), - ], + m = await createTestingModule({ + imports: [StorageModule, QuotaModule], providers: [DocHistoryManager], - }).compile(); + }); - app = m.createNestApplication(); - await app.init(); manager = m.get(DocHistoryManager); Sinon.stub(manager, 'getExpiredDateFromNow').resolves( new Date(Date.now() + 1000) @@ -42,8 +28,7 @@ test.beforeEach(async () => { db = m.get(PrismaService); }); -test.afterEach(async () => { - await app.close(); +test.afterEach.always(async () => { await m.close(); Sinon.restore(); }); diff --git a/packages/backend/server/tests/mailer.e2e.ts b/packages/backend/server/tests/mailer.e2e.ts index 3538d4e303..0e948dfcc9 100644 --- a/packages/backend/server/tests/mailer.e2e.ts +++ b/packages/backend/server/tests/mailer.e2e.ts @@ -6,19 +6,13 @@ import { getCurrentMailMessageCount, getLatestMailMessage, } from '@affine-test/kit/utils/cloud'; -import { Test, TestingModule } from '@nestjs/testing'; +import { TestingModule } from '@nestjs/testing'; import { PrismaClient } from '@prisma/client'; import ava, { type TestFn } from 'ava'; -import { CacheModule } from '../src/cache'; import { ConfigModule } from '../src/config'; -import { RevertCommand, RunCommand } from '../src/data/commands/run'; -import { GqlModule } from '../src/graphql.module'; -import { AuthModule } from '../src/modules/auth'; import { AuthService } from '../src/modules/auth/service'; -import { PrismaModule } from '../src/prisma'; -import { RateLimiterModule } from '../src/throttler'; -import { initFeatureConfigs } from './utils'; +import { createTestingModule } from './utils'; const test = ava as TestFn<{ auth: AuthService; @@ -34,7 +28,7 @@ test.beforeEach(async () => { }); test.beforeEach(async t => { - t.context.module = await Test.createTestingModule({ + t.context.module = await createTestingModule({ imports: [ ConfigModule.forRoot({ auth: { @@ -43,18 +37,9 @@ test.beforeEach(async t => { leeway: 1, }, }), - PrismaModule, - GqlModule, - AuthModule, - CacheModule, - RateLimiterModule, ], - providers: [RevertCommand, RunCommand], - }).compile(); + }); t.context.auth = t.context.module.get(AuthService); - - // init features - await initFeatureConfigs(t.context.module); }); test.afterEach.always(async t => { diff --git a/packages/backend/server/tests/mailer.spec.ts b/packages/backend/server/tests/mailer.spec.ts index 1e1da0c2ef..7f45664ec6 100644 --- a/packages/backend/server/tests/mailer.spec.ts +++ b/packages/backend/server/tests/mailer.spec.ts @@ -1,18 +1,22 @@ import { randomUUID } from 'node:crypto'; import type { INestApplication } from '@nestjs/common'; -import { Test } from '@nestjs/testing'; import { hashSync } from '@node-rs/argon2'; import { type User } from '@prisma/client'; import ava, { type TestFn } from 'ava'; -import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs'; import { AppModule } from '../src/app'; import { MailService } from '../src/modules/auth/mailer'; import { FeatureKind, FeatureManagementService } from '../src/modules/features'; import { Quotas } from '../src/modules/quota'; import { PrismaService } from '../src/prisma'; -import { createWorkspace, getInviteInfo, inviteUser, signUp } from './utils'; +import { + createTestingApp, + createWorkspace, + getInviteInfo, + inviteUser, + signUp, +} from './utils'; const FakePrisma = { fakeUser: { @@ -123,26 +127,20 @@ const test = ava as TestFn<{ }>; test.beforeEach(async t => { - const module = await Test.createTestingModule({ + const { module, app } = await createTestingApp({ imports: [AppModule], - }) - .overrideProvider(PrismaService) - .useValue(FakePrisma) - .overrideProvider(FeatureManagementService) - .useValue({ - hasWorkspaceFeature() { - return false; - }, - }) - .compile(); - const app = module.createNestApplication(); - app.use( - graphqlUploadExpress({ - maxFileSize: 10 * 1024 * 1024, - maxFiles: 5, - }) - ); - await app.init(); + tapModule: module => { + module + .overrideProvider(PrismaService) + .useValue(FakePrisma) + .overrideProvider(FeatureManagementService) + .useValue({ + hasWorkspaceFeature() { + return false; + }, + }); + }, + }); const mail = module.get(MailService); t.context.app = app; diff --git a/packages/backend/server/tests/quota.spec.ts b/packages/backend/server/tests/quota.spec.ts index 5e5237759a..75c7662a04 100644 --- a/packages/backend/server/tests/quota.spec.ts +++ b/packages/backend/server/tests/quota.spec.ts @@ -1,14 +1,8 @@ /// -import { Test, TestingModule } from '@nestjs/testing'; -import { PrismaClient } from '@prisma/client'; +import { TestingModule } from '@nestjs/testing'; import ava, { type TestFn } from 'ava'; -import { CacheModule } from '../src/cache'; -import { ConfigModule } from '../src/config'; -import { RevertCommand, RunCommand } from '../src/data/commands/run'; -import { EventModule } from '../src/event'; -import { AuthModule } from '../src/modules/auth'; import { AuthService } from '../src/modules/auth/service'; import { QuotaManagementService, @@ -18,64 +12,32 @@ import { QuotaType, } from '../src/modules/quota'; import { StorageModule } from '../src/modules/storage'; -import { PrismaModule } from '../src/prisma'; -import { RateLimiterModule } from '../src/throttler'; -import { initFeatureConfigs } from './utils'; +import { createTestingModule } from './utils'; const test = ava as TestFn<{ auth: AuthService; quota: QuotaService; storageQuota: QuotaManagementService; - app: TestingModule; + module: TestingModule; }>; -// cleanup database before each test -test.beforeEach(async () => { - const client = new PrismaClient(); - await client.$connect(); - await client.user.deleteMany({}); - await client.$disconnect(); -}); - test.beforeEach(async t => { - const module = await Test.createTestingModule({ - imports: [ - ConfigModule.forRoot({ - auth: { - accessTokenExpiresIn: 1, - refreshTokenExpiresIn: 1, - leeway: 1, - }, - host: 'example.org', - https: true, - }), - PrismaModule, - CacheModule, - AuthModule, - EventModule, - QuotaModule, - StorageModule, - RateLimiterModule, - RevertCommand, - RunCommand, - ], - }).compile(); + const module = await createTestingModule({ + imports: [StorageModule, QuotaModule], + }); const quota = module.get(QuotaService); const storageQuota = module.get(QuotaManagementService); const auth = module.get(AuthService); - t.context.app = module; + t.context.module = module; t.context.quota = quota; t.context.storageQuota = storageQuota; t.context.auth = auth; - - // init features - await initFeatureConfigs(module); }); test.afterEach.always(async t => { - await t.context.app.close(); + await t.context.module.close(); }); test('should be able to set quota', async t => { diff --git a/packages/backend/server/tests/session.spec.ts b/packages/backend/server/tests/session.spec.ts index 71b08a485a..8814011d39 100644 --- a/packages/backend/server/tests/session.spec.ts +++ b/packages/backend/server/tests/session.spec.ts @@ -1,19 +1,20 @@ /// -import { Test, TestingModule } from '@nestjs/testing'; +import { TestingModule } from '@nestjs/testing'; import ava, { type TestFn } from 'ava'; import { CacheModule } from '../src/cache'; import { ConfigModule } from '../src/config'; import { SessionModule, SessionService } from '../src/session'; +import { createTestingModule } from './utils'; const test = ava as TestFn<{ session: SessionService; - app: TestingModule; + module: TestingModule; }>; test.beforeEach(async t => { - const module = await Test.createTestingModule({ + const module = await createTestingModule({ imports: [ ConfigModule.forRoot({ redis: { @@ -23,14 +24,14 @@ test.beforeEach(async t => { CacheModule, SessionModule, ], - }).compile(); + }); const session = module.get(SessionService); - t.context.app = module; + t.context.module = module; t.context.session = session; }); test.afterEach.always(async t => { - await t.context.app.close(); + await t.context.module.close(); }); test('should be able to set session', async t => { diff --git a/packages/backend/server/tests/user.e2e.ts b/packages/backend/server/tests/user.e2e.ts index 7087ba3f9f..3932282a44 100644 --- a/packages/backend/server/tests/user.e2e.ts +++ b/packages/backend/server/tests/user.e2e.ts @@ -1,40 +1,17 @@ import type { INestApplication } from '@nestjs/common'; -import { Test } from '@nestjs/testing'; -import { PrismaClient } from '@prisma/client'; import test from 'ava'; -import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs'; import request from 'supertest'; import { AppModule } from '../src/app'; -import { RevertCommand, RunCommand } from '../src/data/commands/run'; -import { currentUser, initFeatureConfigs, signUp } from './utils'; +import { createTestingApp, currentUser, signUp } from './utils'; let app: INestApplication; -// cleanup database before each test test.beforeEach(async () => { - const client = new PrismaClient(); - await client.$connect(); - await client.user.deleteMany({}); - await client.$disconnect(); -}); - -test.beforeEach(async () => { - const module = await Test.createTestingModule({ + const { app: testApp } = await createTestingApp({ imports: [AppModule], - providers: [RevertCommand, RunCommand], - }).compile(); - app = module.createNestApplication(); - app.use( - graphqlUploadExpress({ - maxFileSize: 10 * 1024 * 1024, - maxFiles: 5, - }) - ); - await app.init(); - - // init features - await initFeatureConfigs(module); + }); + app = testApp; }); test.afterEach.always(async () => { diff --git a/packages/backend/server/tests/utils/utils.ts b/packages/backend/server/tests/utils/utils.ts index 042f718c93..334a78941e 100644 --- a/packages/backend/server/tests/utils/utils.ts +++ b/packages/backend/server/tests/utils/utils.ts @@ -1,14 +1,14 @@ -import { randomUUID } from 'node:crypto'; +import { INestApplication, ModuleMetadata } from '@nestjs/common'; +import { Query, Resolver } from '@nestjs/graphql'; +import { Test, TestingModuleBuilder } from '@nestjs/testing'; +import { PrismaClient } from '@prisma/client'; +import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs'; -import { TestingModule } from '@nestjs/testing'; -import { hashSync } from '@node-rs/argon2'; -import { PrismaClient, type User } from '@prisma/client'; +import { AppModule, FunctionalityModules } from '../../src/app'; +import { UserFeaturesInit1698652531198 } from '../../src/data/migrations/1698652531198-user-features-init'; +import { GqlModule } from '../../src/graphql.module'; -import { RevertCommand, RunCommand } from '../../src/data/commands/run'; - -export async function flushDB() { - const client = new PrismaClient(); - await client.$connect(); +async function flushDB(client: PrismaClient) { const result: { tablename: string }[] = await client.$queryRaw`SELECT tablename FROM pg_catalog.pg_tables @@ -22,41 +22,99 @@ export async function flushDB() { .filter(name => !name.includes('migrations')) .join(', ')}` ); - - await client.$disconnect(); } -export class FakePrisma { - fakeUser: User = { - id: randomUUID(), - name: 'Alex Yang', - avatarUrl: '', - email: 'alex.yang@example.org', - password: hashSync('123456'), - emailVerified: new Date(), - createdAt: new Date(), - }; +async function initFeatureConfigs(db: PrismaClient) { + await UserFeaturesInit1698652531198.up(db); +} - get user() { - // eslint-disable-next-line @typescript-eslint/no-this-alias - const prisma = this; - return { - async findFirst() { - return prisma.fakeUser; - }, - async findUnique() { - return this.findFirst(); - }, - async update() { - return this.findFirst(); - }, - }; +export async function initTestingDB(db: PrismaClient) { + await flushDB(db); + await initFeatureConfigs(db); +} + +interface TestingModuleMeatdata extends ModuleMetadata { + tapModule?(m: TestingModuleBuilder): void; + tapApp?(app: INestApplication): void; +} + +function dedupeModules(modules: NonNullable) { + const map = new Map(); + + modules.forEach(m => { + if ('module' in m) { + map.set(m.module, m); + } else { + map.set(m, m); + } + }); + + return Array.from(map.values()); +} + +@Resolver(() => String) +class MockResolver { + @Query(() => String) + hello() { + return 'hello world'; } } -export async function initFeatureConfigs(module: TestingModule) { - const run = module.get(RunCommand); - const revert = module.get(RevertCommand); - await Promise.allSettled([revert.run(['UserFeaturesInit1698652531198'])]); - await run.runOne('UserFeaturesInit1698652531198'); +export async function createTestingModule( + moduleDef: TestingModuleMeatdata = {} +) { + // setting up + let imports = moduleDef.imports ?? []; + imports = + imports[0] === AppModule + ? [AppModule] + : dedupeModules([...FunctionalityModules, GqlModule, ...imports]); + + const builder = Test.createTestingModule({ + imports, + providers: [MockResolver, ...(moduleDef.providers ?? [])], + controllers: moduleDef.controllers, + }); + + if (moduleDef.tapModule) { + moduleDef.tapModule(builder); + } + + const m = await builder.compile(); + + const prisma = m.get(PrismaClient); + if (prisma instanceof PrismaClient) { + await flushDB(prisma); + await initFeatureConfigs(prisma); + } + + return m; +} + +export async function createTestingApp(moduleDef: TestingModuleMeatdata = {}) { + const m = await createTestingModule(moduleDef); + + const app = m.createNestApplication({ + cors: true, + bodyParser: true, + rawBody: true, + }); + + app.use( + graphqlUploadExpress({ + maxFileSize: 10 * 1024 * 1024, + maxFiles: 5, + }) + ); + + if (moduleDef.tapApp) { + moduleDef.tapApp(app); + } + + await app.init(); + + return { + module: m, + app, + }; } diff --git a/packages/backend/server/tests/workspace-blobs.spec.ts b/packages/backend/server/tests/workspace-blobs.spec.ts index ed86723e8a..37abda1e45 100644 --- a/packages/backend/server/tests/workspace-blobs.spec.ts +++ b/packages/backend/server/tests/workspace-blobs.spec.ts @@ -1,19 +1,15 @@ import type { INestApplication } from '@nestjs/common'; -import { Test } from '@nestjs/testing'; -import { PrismaClient } from '@prisma/client'; import test from 'ava'; -import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs'; import request from 'supertest'; import { AppModule } from '../src/app'; -import { RevertCommand, RunCommand } from '../src/data/commands/run'; import { QuotaService, QuotaType } from '../src/modules/quota'; import { checkBlobSize, collectAllBlobSizes, + createTestingApp, createWorkspace, getWorkspaceBlobsSize, - initFeatureConfigs, listBlobs, setBlob, signUp, @@ -22,36 +18,13 @@ import { let app: INestApplication; let quota: QuotaService; -const client = new PrismaClient(); - -// cleanup database before each test test.beforeEach(async () => { - await client.$connect(); - await client.user.deleteMany({}); - await client.snapshot.deleteMany({}); - await client.update.deleteMany({}); - await client.workspace.deleteMany({}); - await client.$disconnect(); -}); - -test.beforeEach(async () => { - const module = await Test.createTestingModule({ + const { app: testApp } = await createTestingApp({ imports: [AppModule], - providers: [RevertCommand, RunCommand], - }).compile(); - app = module.createNestApplication(); - app.use( - graphqlUploadExpress({ - maxFileSize: 10 * 1024 * 1024, - maxFiles: 5, - }) - ); - quota = module.get(QuotaService); + }); - // init features - await initFeatureConfigs(module); - - await app.init(); + app = testApp; + quota = app.get(QuotaService); }); test.afterEach.always(async () => { diff --git a/packages/backend/server/tests/workspace-invite.e2e.ts b/packages/backend/server/tests/workspace-invite.e2e.ts index 52a6eac694..8ecc747290 100644 --- a/packages/backend/server/tests/workspace-invite.e2e.ts +++ b/packages/backend/server/tests/workspace-invite.e2e.ts @@ -3,20 +3,18 @@ import { getLatestMailMessage, } from '@affine-test/kit/utils/cloud'; import type { INestApplication } from '@nestjs/common'; -import { Test } from '@nestjs/testing'; import { PrismaClient } from '@prisma/client'; import ava, { type TestFn } from 'ava'; -import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs'; import { AppModule } from '../src/app'; -import { RevertCommand, RunCommand } from '../src/data/commands/run'; import { MailService } from '../src/modules/auth/mailer'; import { AuthService } from '../src/modules/auth/service'; +import { PrismaService } from '../src/prisma'; import { acceptInviteById, + createTestingApp, createWorkspace, getWorkspace, - initFeatureConfigs, inviteUser, leaveWorkspace, revokeUser, @@ -31,36 +29,13 @@ const test = ava as TestFn<{ }>; test.beforeEach(async t => { - const client = new PrismaClient(); - t.context.client = client; - await client.$connect(); - await client.user.deleteMany({}); - await client.snapshot.deleteMany({}); - await client.update.deleteMany({}); - await client.workspace.deleteMany({}); - await client.$disconnect(); - const module = await Test.createTestingModule({ + const { app } = await createTestingApp({ imports: [AppModule], - providers: [RevertCommand, RunCommand], - }).compile(); - const app = module.createNestApplication(); - app.use( - graphqlUploadExpress({ - maxFileSize: 10 * 1024 * 1024, - maxFiles: 5, - }) - ); - await app.init(); - - const auth = module.get(AuthService); - const mail = module.get(MailService); - + }); t.context.app = app; - t.context.auth = auth; - t.context.mail = mail; - - // init features - await initFeatureConfigs(module); + t.context.client = app.get(PrismaService); + t.context.auth = app.get(AuthService); + t.context.mail = app.get(MailService); }); test.afterEach.always(async t => { diff --git a/packages/backend/server/tests/workspace.e2e.ts b/packages/backend/server/tests/workspace.e2e.ts index 86904acc6a..8483a92141 100644 --- a/packages/backend/server/tests/workspace.e2e.ts +++ b/packages/backend/server/tests/workspace.e2e.ts @@ -1,19 +1,17 @@ import type { INestApplication } from '@nestjs/common'; -import { Test } from '@nestjs/testing'; import { PrismaClient } from '@prisma/client'; import ava, { type TestFn } from 'ava'; -import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs'; import request from 'supertest'; import { AppModule } from '../src/app'; -import { RevertCommand, RunCommand } from '../src/data/commands/run'; +import { PrismaService } from '../src/prisma'; import { acceptInviteById, + createTestingApp, createWorkspace, currentUser, getPublicWorkspace, getWorkspacePublicPages, - initFeatureConfigs, inviteUser, publishPage, revokePublicPage, @@ -27,30 +25,12 @@ const test = ava as TestFn<{ }>; test.beforeEach(async t => { - const client = new PrismaClient(); - await client.$connect(); - await client.user.deleteMany({}); - await client.update.deleteMany({}); - await client.snapshot.deleteMany({}); - await client.workspace.deleteMany({}); - await client.$disconnect(); - const module = await Test.createTestingModule({ + const { app } = await createTestingApp({ imports: [AppModule], - providers: [RevertCommand, RunCommand], - }).compile(); - const app = module.createNestApplication(); - app.use( - graphqlUploadExpress({ - maxFileSize: 10 * 1024 * 1024, - maxFiles: 5, - }) - ); - await app.init(); - t.context.client = client; - t.context.app = app; + }); - // init features - await initFeatureConfigs(module); + t.context.client = app.get(PrismaService); + t.context.app = app; }); test.afterEach.always(async t => { From d1c2b2a7b0389ed3077eaf46f4faa924f1f3d9ef Mon Sep 17 00:00:00 2001 From: EYHN Date: Thu, 11 Jan 2024 09:47:24 +0000 Subject: [PATCH 08/92] fix(core): workspace not found after import (#5571) close TOV-393 --- .../src/components/affine/create-workspace-modal/index.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/frontend/core/src/components/affine/create-workspace-modal/index.tsx b/packages/frontend/core/src/components/affine/create-workspace-modal/index.tsx index 0f28f86522..df58adbfaa 100644 --- a/packages/frontend/core/src/components/affine/create-workspace-modal/index.tsx +++ b/packages/frontend/core/src/components/affine/create-workspace-modal/index.tsx @@ -121,6 +121,9 @@ export const CreateWorkspaceModal = ({ const result = await apis.dialog.loadDBFile(); if (result.workspaceId && !canceled) { _addLocalWorkspace(result.workspaceId); + workspaceManager.list.revalidate().catch(err => { + logger.error("can't revalidate workspace list", err); + }); onCreate(result.workspaceId); } else if (result.error || result.canceled) { if (result.error) { From d6f65ea4142219679a5e8a2fff7c0e35ba941282 Mon Sep 17 00:00:00 2001 From: DarkSky Date: Thu, 11 Jan 2024 10:21:40 +0000 Subject: [PATCH 09/92] feat: blob size limit with quota (#5524) fix AFF-506 TOV-342 --- packages/backend/server/package.json | 1 + .../server/src/modules/quota/schema.ts | 17 +++++ .../server/src/modules/quota/storage.ts | 4 +- .../backend/server/src/modules/quota/types.ts | 8 +- .../src/modules/workspaces/resolvers/blob.ts | 75 +++++++++++++++---- .../modules/workspaces/resolvers/workspace.ts | 12 ++- packages/backend/server/src/schema.gql | 3 + .../server/tests/workspace-blobs.spec.ts | 56 ++++++++++++-- 8 files changed, 150 insertions(+), 26 deletions(-) diff --git a/packages/backend/server/package.json b/packages/backend/server/package.json index bc479fa867..7b85432795 100644 --- a/packages/backend/server/package.json +++ b/packages/backend/server/package.json @@ -114,6 +114,7 @@ "typescript": "^5.3.2" }, "ava": { + "timeout": "1m", "extensions": { "ts": "module" }, diff --git a/packages/backend/server/src/modules/quota/schema.ts b/packages/backend/server/src/modules/quota/schema.ts index a24f0db52a..10e4dbad79 100644 --- a/packages/backend/server/src/modules/quota/schema.ts +++ b/packages/backend/server/src/modules/quota/schema.ts @@ -37,6 +37,23 @@ export const Quotas: Quota[] = [ memberLimit: 10, }, }, + { + feature: QuotaType.RestrictedPlanV1, + type: FeatureKind.Quota, + version: 1, + configs: { + // quota name + name: 'Restricted', + // single blob limit 10MB + blobLimit: OneMB, + // total blob limit 1GB + storageQuota: 10 * OneMB, + // history period of validity 30 days + historyPeriod: 30 * OneDay, + // member limit 10 + memberLimit: 10, + }, + }, ]; export const Quota_FreePlanV1 = { diff --git a/packages/backend/server/src/modules/quota/storage.ts b/packages/backend/server/src/modules/quota/storage.ts index dde3620626..fa4fdcd28b 100644 --- a/packages/backend/server/src/modules/quota/storage.ts +++ b/packages/backend/server/src/modules/quota/storage.ts @@ -44,11 +44,11 @@ export class QuotaManagementService { const { user: owner } = await this.permissions.getWorkspaceOwner(workspaceId); if (!owner) throw new NotFoundException('Workspace owner not found'); - const { storageQuota } = await this.getUserQuota(owner.id); + const { storageQuota, blobLimit } = await this.getUserQuota(owner.id); // get all workspaces size of owner used const usageSize = await this.getUserUsage(owner.id); - return { quota: storageQuota, size: usageSize }; + return { quota: storageQuota, size: usageSize, limit: blobLimit }; } async checkBlobQuota(workspaceId: string, size: number) { diff --git a/packages/backend/server/src/modules/quota/types.ts b/packages/backend/server/src/modules/quota/types.ts index 6097d6bc57..5f94952713 100644 --- a/packages/backend/server/src/modules/quota/types.ts +++ b/packages/backend/server/src/modules/quota/types.ts @@ -8,10 +8,16 @@ import { ByteUnit, OneDay, OneKB } from './constant'; export enum QuotaType { FreePlanV1 = 'free_plan_v1', ProPlanV1 = 'pro_plan_v1', + // only for test, smaller quota + RestrictedPlanV1 = 'restricted_plan_v1', } const quotaPlan = z.object({ - feature: z.enum([QuotaType.FreePlanV1, QuotaType.ProPlanV1]), + feature: z.enum([ + QuotaType.FreePlanV1, + QuotaType.ProPlanV1, + QuotaType.RestrictedPlanV1, + ]), configs: z.object({ name: z.string(), blobLimit: z.number().positive().int(), diff --git a/packages/backend/server/src/modules/workspaces/resolvers/blob.ts b/packages/backend/server/src/modules/workspaces/resolvers/blob.ts index d2a03994ac..ae19169dcb 100644 --- a/packages/backend/server/src/modules/workspaces/resolvers/blob.ts +++ b/packages/backend/server/src/modules/workspaces/resolvers/blob.ts @@ -1,4 +1,4 @@ -import { ForbiddenException, Logger, UseGuards } from '@nestjs/common'; +import { HttpStatus, Logger, UseGuards } from '@nestjs/common'; import { Args, Float, @@ -9,12 +9,14 @@ import { ResolveField, Resolver, } from '@nestjs/graphql'; +import { GraphQLError } from 'graphql'; import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs'; import { MakeCache, PreventCache } from '../../../cache'; import { CloudThrottlerGuard } from '../../../throttler'; import type { FileUpload } from '../../../types'; import { Auth, CurrentUser } from '../../auth'; +import { FeatureManagementService, FeatureType } from '../../features'; import { QuotaManagementService } from '../../quota'; import { WorkspaceBlobStorage } from '../../storage'; import { UserType } from '../../users'; @@ -28,10 +30,26 @@ export class WorkspaceBlobResolver { logger = new Logger(WorkspaceBlobResolver.name); constructor( private readonly permissions: PermissionService, + private readonly feature: FeatureManagementService, private readonly quota: QuotaManagementService, private readonly storage: WorkspaceBlobStorage ) {} + @ResolveField(() => [String], { + description: 'List blobs of workspace', + complexity: 2, + }) + async blobs( + @CurrentUser() user: UserType, + @Parent() workspace: WorkspaceType + ) { + await this.permissions.checkWorkspace(workspace.id, user.id); + + return this.storage + .list(workspace.id) + .then(list => list.map(item => item.key)); + } + @ResolveField(() => Int, { description: 'Blobs size of workspace', complexity: 2, @@ -107,16 +125,30 @@ export class WorkspaceBlobResolver { Permission.Write ); - const { quota, size } = await this.quota.getWorkspaceUsage(workspaceId); + const { quota, size, limit } = + await this.quota.getWorkspaceUsage(workspaceId); + + const unlimited = await this.feature.hasWorkspaceFeature( + workspaceId, + FeatureType.UnlimitedWorkspace + ); const checkExceeded = (recvSize: number) => { if (!quota) { - throw new ForbiddenException('cannot find user quota'); + throw new GraphQLError('cannot find user quota', { + extensions: { + status: HttpStatus[HttpStatus.FORBIDDEN], + code: HttpStatus.FORBIDDEN, + }, + }); } - if (size + recvSize > quota) { - this.logger.log( - `storage size limit exceeded: ${size + recvSize} > ${quota}` - ); + const total = size + recvSize; + // only skip total storage check if workspace has unlimited feature + if (total > quota && !unlimited) { + this.logger.log(`storage size limit exceeded: ${total} > ${quota}`); + return true; + } else if (recvSize > limit) { + this.logger.log(`blob size limit exceeded: ${recvSize} > ${limit}`); return true; } else { return false; @@ -124,7 +156,12 @@ export class WorkspaceBlobResolver { }; if (checkExceeded(0)) { - throw new ForbiddenException('storage size limit exceeded'); + throw new GraphQLError('storage or blob size limit exceeded', { + extensions: { + status: HttpStatus[HttpStatus.PAYLOAD_TOO_LARGE], + code: HttpStatus.PAYLOAD_TOO_LARGE, + }, + }); } const buffer = await new Promise((resolve, reject) => { const stream = blob.createReadStream(); @@ -135,7 +172,14 @@ export class WorkspaceBlobResolver { // check size after receive each chunk to avoid unnecessary memory usage const bufferSize = chunks.reduce((acc, cur) => acc + cur.length, 0); if (checkExceeded(bufferSize)) { - reject(new ForbiddenException('storage size limit exceeded')); + reject( + new GraphQLError('storage or blob size limit exceeded', { + extensions: { + status: HttpStatus[HttpStatus.PAYLOAD_TOO_LARGE], + code: HttpStatus.PAYLOAD_TOO_LARGE, + }, + }) + ); } }); stream.on('error', reject); @@ -143,17 +187,20 @@ export class WorkspaceBlobResolver { const buffer = Buffer.concat(chunks); if (checkExceeded(buffer.length)) { - reject(new ForbiddenException('storage size limit exceeded')); + reject( + new GraphQLError('storage limit exceeded', { + extensions: { + status: HttpStatus[HttpStatus.PAYLOAD_TOO_LARGE], + code: HttpStatus.PAYLOAD_TOO_LARGE, + }, + }) + ); } else { resolve(buffer); } }); }); - if (!(await this.quota.checkBlobQuota(workspaceId, buffer.length))) { - throw new ForbiddenException('blob size limit exceeded'); - } - await this.storage.put(workspaceId, blob.filename, buffer); return blob.filename; } diff --git a/packages/backend/server/src/modules/workspaces/resolvers/workspace.ts b/packages/backend/server/src/modules/workspaces/resolvers/workspace.ts index 806952155b..328744e628 100644 --- a/packages/backend/server/src/modules/workspaces/resolvers/workspace.ts +++ b/packages/backend/server/src/modules/workspaces/resolvers/workspace.ts @@ -1,7 +1,6 @@ import { ForbiddenException, HttpStatus, - InternalServerErrorException, Logger, NotFoundException, UseGuards, @@ -398,8 +397,15 @@ export class WorkspaceResolver { `failed to send ${workspaceId} invite email to ${email}, but successfully revoked permission: ${e}` ); } - - return new InternalServerErrorException(e); + return new GraphQLError( + 'failed to send invite email, please try again', + { + extensions: { + status: HttpStatus[HttpStatus.INTERNAL_SERVER_ERROR], + code: HttpStatus.INTERNAL_SERVER_ERROR, + }, + } + ); } } return inviteId; diff --git a/packages/backend/server/src/schema.gql b/packages/backend/server/src/schema.gql index c16ac448b9..57b78e22d9 100644 --- a/packages/backend/server/src/schema.gql +++ b/packages/backend/server/src/schema.gql @@ -144,6 +144,9 @@ type WorkspaceType { publicPages: [WorkspacePage!]! histories(guid: String!, before: DateTime, take: Int): [DocHistoryType!]! + """List blobs of workspace""" + blobs: [String!]! + """Blobs size of workspace""" blobsSize: Int! } diff --git a/packages/backend/server/tests/workspace-blobs.spec.ts b/packages/backend/server/tests/workspace-blobs.spec.ts index 37abda1e45..4a1969ed59 100644 --- a/packages/backend/server/tests/workspace-blobs.spec.ts +++ b/packages/backend/server/tests/workspace-blobs.spec.ts @@ -3,6 +3,7 @@ import test from 'ava'; import request from 'supertest'; import { AppModule } from '../src/app'; +import { FeatureManagementService, FeatureType } from '../src/modules/features'; import { QuotaService, QuotaType } from '../src/modules/quota'; import { checkBlobSize, @@ -15,8 +16,11 @@ import { signUp, } from './utils'; +const OneMB = 1024 * 1024; + let app: INestApplication; let quota: QuotaService; +let feature: FeatureManagementService; test.beforeEach(async () => { const { app: testApp } = await createTestingApp({ @@ -25,6 +29,7 @@ test.beforeEach(async () => { app = testApp; quota = app.get(QuotaService); + feature = app.get(FeatureManagementService); }); test.afterEach.always(async () => { @@ -163,12 +168,51 @@ test('should be able calc quota after switch plan', async t => { t.is(size2, 0, 'failed to check pro plan blob size'); }); -test('should reject blob exceeded limit', t => { - // TODO - t.true(true); +test('should reject blob exceeded limit', async t => { + const u1 = await signUp(app, 'darksky', 'darksky@affine.pro', '1'); + + const workspace1 = await createWorkspace(app, u1.token.token); + await quota.switchUserQuota(u1.id, QuotaType.RestrictedPlanV1); + + const buffer1 = Buffer.from(Array.from({ length: OneMB + 1 }, () => 0)); + await t.throwsAsync(setBlob(app, u1.token.token, workspace1.id, buffer1)); + + await quota.switchUserQuota(u1.id, QuotaType.FreePlanV1); + + const buffer2 = Buffer.from(Array.from({ length: OneMB + 1 }, () => 0)); + await t.notThrowsAsync(setBlob(app, u1.token.token, workspace1.id, buffer2)); + + const buffer3 = Buffer.from(Array.from({ length: 10 * OneMB + 1 }, () => 0)); + await t.throwsAsync(setBlob(app, u1.token.token, workspace1.id, buffer3)); }); -test('should reject blob exceeded quota', t => { - // TODO - t.true(true); +test('should reject blob exceeded quota', async t => { + const u1 = await signUp(app, 'darksky', 'darksky@affine.pro', '1'); + + const workspace = await createWorkspace(app, u1.token.token); + await quota.switchUserQuota(u1.id, QuotaType.RestrictedPlanV1); + + const buffer = Buffer.from(Array.from({ length: OneMB }, () => 0)); + + for (let i = 0; i < 10; i++) { + await t.notThrowsAsync(setBlob(app, u1.token.token, workspace.id, buffer)); + } + + await t.throwsAsync(setBlob(app, u1.token.token, workspace.id, buffer)); +}); + +test('should accept blob even storage out of quota if workspace has unlimited feature', async t => { + const u1 = await signUp(app, 'darksky', 'darksky@affine.pro', '1'); + + const workspace = await createWorkspace(app, u1.token.token); + await quota.switchUserQuota(u1.id, QuotaType.RestrictedPlanV1); + feature.addWorkspaceFeatures(workspace.id, FeatureType.UnlimitedWorkspace); + + const buffer = Buffer.from(Array.from({ length: OneMB }, () => 0)); + + for (let i = 0; i < 10; i++) { + await t.notThrowsAsync(setBlob(app, u1.token.token, workspace.id, buffer)); + } + + await t.notThrowsAsync(setBlob(app, u1.token.token, workspace.id, buffer)); }); From 89b5c96d25a82337b22ac290d046d19985c3490c Mon Sep 17 00:00:00 2001 From: liuyi Date: Fri, 12 Jan 2024 04:18:39 +0000 Subject: [PATCH 10/92] refactor(server): folder structure (#5573) --- packages/backend/server/src/affine.env.ts | 12 --- packages/backend/server/src/affine.ts | 3 - packages/backend/server/src/app.controller.ts | 9 +- packages/backend/server/src/app.ts | 24 +++-- .../backend/server/src/config/affine.env.ts | 41 +++++++++ .../{affine.config.ts => config/affine.ts} | 9 +- packages/backend/server/src/config/env.ts | 17 ---- packages/backend/server/src/constants.ts | 3 - packages/backend/server/src/data/app.ts | 5 +- .../backend/server/src/data/commands/run.ts | 11 ++- .../src/data/migrations/1698398506533-guid.ts | 2 +- .../src/{ => fundamentals}/cache/index.ts | 3 +- .../src/{ => fundamentals}/cache/instances.ts | 0 .../{ => fundamentals}/cache/interceptor.ts | 0 .../cache/providers/cache.ts | 0 .../cache/providers/redis.ts | 0 .../{ => fundamentals}/cache/redis/index.ts | 0 .../src/{ => fundamentals}/config/def.ts | 40 ++------- .../src/{ => fundamentals}/config/default.ts | 58 ++---------- .../server/src/fundamentals/config/env.ts | 50 +++++++++++ .../server/src/fundamentals/config/index.ts | 6 ++ .../config/module.ts} | 27 ++---- .../config/storage/index.ts | 0 .../src/{ => fundamentals}/event/events.ts | 0 .../src/{ => fundamentals}/event/index.ts | 0 .../src/{ => fundamentals}/event/types.ts | 0 .../graphql/index.ts} | 16 ++-- .../graphql/logger-plugin.ts | 15 ++-- .../backend/server/src/fundamentals/index.ts | 20 +++++ .../server/src/fundamentals/mailer/index.ts | 12 +++ .../mailer/mail.service.ts | 2 +- .../auth => fundamentals}/mailer/mailer.ts | 2 +- .../auth => fundamentals}/mailer/template.ts | 0 .../src/{ => fundamentals}/metrics/index.ts | 3 +- .../src/{ => fundamentals}/metrics/metrics.ts | 0 .../metrics/opentelemetry.ts | 0 .../src/{ => fundamentals}/metrics/prisma.ts | 0 .../src/{ => fundamentals}/metrics/utils.ts | 0 .../src/{ => fundamentals}/prisma/index.ts | 0 .../src/{ => fundamentals}/prisma/service.ts | 0 .../session/index.ts} | 2 +- .../storage/__tests__/fs.spec.ts | 0 .../src/{ => fundamentals}/storage/index.ts | 17 +++- .../storage/providers/fs.ts | 2 +- .../storage/providers/index.ts | 2 +- .../storage/providers/provider.ts | 2 +- .../storage/providers/r2.ts | 2 +- .../storage/providers/s3.ts | 2 +- .../storage/providers/utils.ts | 0 .../throttler/index.ts} | 6 +- .../utils/request.ts} | 0 .../src/{ => fundamentals}/utils/types.ts | 20 +++++ .../src/fundamentals/websocket/index.ts | 1 + .../websocket}/redis-adapter.ts | 0 packages/backend/server/src/index.ts | 25 ++---- .../server/src/middleware/exception-logger.ts | 5 +- .../backend/server/src/modules/auth/guard.ts | 6 +- .../backend/server/src/modules/auth/index.ts | 14 +-- .../server/src/modules/auth/mailer/index.ts | 2 - .../src/modules/auth/next-auth-options.ts | 10 ++- .../src/modules/auth/next-auth.controller.ts | 13 +-- .../server/src/modules/auth/resolver.ts | 9 +- .../server/src/modules/auth/service.ts | 11 ++- .../server/src/modules/auth/utils/jwt.ts | 3 +- .../src/modules/auth/utils/send-mail.ts | 4 +- packages/backend/server/src/modules/config.ts | 4 +- .../backend/server/src/modules/doc/history.ts | 11 ++- .../backend/server/src/modules/doc/manager.ts | 16 ++-- .../server/src/modules/features/feature.ts | 5 +- .../server/src/modules/features/index.ts | 3 +- .../server/src/modules/features/management.ts | 3 +- .../server/src/modules/features/service.ts | 2 +- packages/backend/server/src/modules/index.ts | 7 +- .../server/src/modules/payment/resolver.ts | 3 +- .../server/src/modules/payment/service.ts | 3 +- .../server/src/modules/payment/stripe.ts | 2 +- .../server/src/modules/payment/webhook.ts | 2 +- .../backend/server/src/modules/quota/quota.ts | 2 +- .../server/src/modules/quota/service.ts | 2 +- .../src/modules/storage/wrappers/avatar.ts | 11 ++- .../src/modules/storage/wrappers/blob.ts | 18 ++-- .../src/modules/sync/events/events.gateway.ts | 5 +- .../backend/server/src/modules/sync/utils.ts | 11 --- .../server/src/modules/users/management.ts | 2 +- .../server/src/modules/users/resolver.ts | 11 ++- .../backend/server/src/modules/users/users.ts | 2 +- .../{ => modules}/utils/__tests__/doc.spec.ts | 0 .../server/src/{ => modules}/utils/doc.ts | 0 .../src/modules/workspaces/controller.ts | 5 +- .../src/modules/workspaces/management.ts | 2 +- .../src/modules/workspaces/permission.ts | 2 +- .../src/modules/workspaces/resolvers/blob.ts | 9 +- .../modules/workspaces/resolvers/history.ts | 6 +- .../src/modules/workspaces/resolvers/page.ts | 5 +- .../modules/workspaces/resolvers/workspace.ts | 13 +-- packages/backend/server/src/prelude.ts | 45 ++++++++-- packages/backend/server/src/types.ts | 14 --- packages/backend/server/tests/app.e2e.ts | 5 +- packages/backend/server/tests/auth.e2e.ts | 2 +- packages/backend/server/tests/auth.spec.ts | 7 +- packages/backend/server/tests/cache.spec.ts | 4 +- packages/backend/server/tests/config.spec.ts | 2 +- packages/backend/server/tests/doc.spec.ts | 12 +-- .../server/tests/exception-logger.e2e.ts | 89 ------------------- packages/backend/server/tests/feature.spec.ts | 6 +- packages/backend/server/tests/history.spec.ts | 8 +- packages/backend/server/tests/mailer.e2e.ts | 2 +- packages/backend/server/tests/mailer.spec.ts | 6 +- packages/backend/server/tests/quota.spec.ts | 2 +- packages/backend/server/tests/session.spec.ts | 6 +- packages/backend/server/tests/utils/utils.ts | 2 +- .../server/tests/workspace-invite.e2e.ts | 5 +- .../backend/server/tests/workspace.e2e.ts | 3 +- 113 files changed, 453 insertions(+), 482 deletions(-) delete mode 100644 packages/backend/server/src/affine.env.ts delete mode 100644 packages/backend/server/src/affine.ts create mode 100644 packages/backend/server/src/config/affine.env.ts rename packages/backend/server/src/{affine.config.ts => config/affine.ts} (87%) delete mode 100644 packages/backend/server/src/config/env.ts delete mode 100644 packages/backend/server/src/constants.ts rename packages/backend/server/src/{ => fundamentals}/cache/index.ts (89%) rename packages/backend/server/src/{ => fundamentals}/cache/instances.ts (100%) rename packages/backend/server/src/{ => fundamentals}/cache/interceptor.ts (100%) rename packages/backend/server/src/{ => fundamentals}/cache/providers/cache.ts (100%) rename packages/backend/server/src/{ => fundamentals}/cache/providers/redis.ts (100%) rename packages/backend/server/src/{ => fundamentals}/cache/redis/index.ts (100%) rename packages/backend/server/src/{ => fundamentals}/config/def.ts (90%) rename packages/backend/server/src/{ => fundamentals}/config/default.ts (63%) create mode 100644 packages/backend/server/src/fundamentals/config/env.ts create mode 100644 packages/backend/server/src/fundamentals/config/index.ts rename packages/backend/server/src/{config/index.ts => fundamentals/config/module.ts} (68%) rename packages/backend/server/src/{ => fundamentals}/config/storage/index.ts (100%) rename packages/backend/server/src/{ => fundamentals}/event/events.ts (100%) rename packages/backend/server/src/{ => fundamentals}/event/index.ts (100%) rename packages/backend/server/src/{ => fundamentals}/event/types.ts (100%) rename packages/backend/server/src/{graphql.module.ts => fundamentals/graphql/index.ts} (72%) rename packages/backend/server/src/{ => fundamentals}/graphql/logger-plugin.ts (79%) create mode 100644 packages/backend/server/src/fundamentals/index.ts create mode 100644 packages/backend/server/src/fundamentals/mailer/index.ts rename packages/backend/server/src/{modules/auth => fundamentals}/mailer/mail.service.ts (99%) rename packages/backend/server/src/{modules/auth => fundamentals}/mailer/mailer.ts (95%) rename packages/backend/server/src/{modules/auth => fundamentals}/mailer/template.ts (100%) rename packages/backend/server/src/{ => fundamentals}/metrics/index.ts (90%) rename packages/backend/server/src/{ => fundamentals}/metrics/metrics.ts (100%) rename packages/backend/server/src/{ => fundamentals}/metrics/opentelemetry.ts (100%) rename packages/backend/server/src/{ => fundamentals}/metrics/prisma.ts (100%) rename packages/backend/server/src/{ => fundamentals}/metrics/utils.ts (100%) rename packages/backend/server/src/{ => fundamentals}/prisma/index.ts (100%) rename packages/backend/server/src/{ => fundamentals}/prisma/service.ts (100%) rename packages/backend/server/src/{session.ts => fundamentals/session/index.ts} (95%) rename packages/backend/server/src/{modules => fundamentals}/storage/__tests__/fs.spec.ts (100%) rename packages/backend/server/src/{ => fundamentals}/storage/index.ts (69%) rename packages/backend/server/src/{modules => fundamentals}/storage/providers/fs.ts (99%) rename packages/backend/server/src/{modules => fundamentals}/storage/providers/index.ts (93%) rename packages/backend/server/src/{modules => fundamentals}/storage/providers/provider.ts (94%) rename packages/backend/server/src/{modules => fundamentals}/storage/providers/r2.ts (89%) rename packages/backend/server/src/{modules => fundamentals}/storage/providers/s3.ts (98%) rename packages/backend/server/src/{modules => fundamentals}/storage/providers/utils.ts (100%) rename packages/backend/server/src/{throttler.ts => fundamentals/throttler/index.ts} (93%) rename packages/backend/server/src/{utils/nestjs.ts => fundamentals/utils/request.ts} (100%) rename packages/backend/server/src/{ => fundamentals}/utils/types.ts (71%) create mode 100644 packages/backend/server/src/fundamentals/websocket/index.ts rename packages/backend/server/src/{modules/sync => fundamentals/websocket}/redis-adapter.ts (100%) delete mode 100644 packages/backend/server/src/modules/auth/mailer/index.ts delete mode 100644 packages/backend/server/src/modules/sync/utils.ts rename packages/backend/server/src/{ => modules}/utils/__tests__/doc.spec.ts (100%) rename packages/backend/server/src/{ => modules}/utils/doc.ts (100%) delete mode 100644 packages/backend/server/src/types.ts delete mode 100644 packages/backend/server/tests/exception-logger.e2e.ts diff --git a/packages/backend/server/src/affine.env.ts b/packages/backend/server/src/affine.env.ts deleted file mode 100644 index 5605964bf5..0000000000 --- a/packages/backend/server/src/affine.env.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { homedir } from 'node:os'; -import { join } from 'node:path'; - -import { config } from 'dotenv'; - -import { SERVER_FLAVOR } from './config/default'; - -if (SERVER_FLAVOR === 'selfhosted') { - config({ path: join(homedir(), '.affine', '.env') }); -} else { - config(); -} diff --git a/packages/backend/server/src/affine.ts b/packages/backend/server/src/affine.ts deleted file mode 100644 index ceb37536da..0000000000 --- a/packages/backend/server/src/affine.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { getDefaultAFFiNEConfig } from './config/default'; - -globalThis.AFFiNE = getDefaultAFFiNEConfig(); diff --git a/packages/backend/server/src/app.controller.ts b/packages/backend/server/src/app.controller.ts index 43eb338179..87f3f981df 100644 --- a/packages/backend/server/src/app.controller.ts +++ b/packages/backend/server/src/app.controller.ts @@ -1,13 +1,16 @@ import { Controller, Get } from '@nestjs/common'; +import { Config } from './fundamentals/config'; + @Controller('/') export class AppController { + constructor(private readonly config: Config) {} + @Get() info() { - const version = AFFiNE.version; return { - compatibility: version, - message: `AFFiNE ${version} Server`, + compatibility: this.config.version, + message: `AFFiNE ${this.config.version} Server`, }; } } diff --git a/packages/backend/server/src/app.ts b/packages/backend/server/src/app.ts index ea47c41a32..8efc1b1f32 100644 --- a/packages/backend/server/src/app.ts +++ b/packages/backend/server/src/app.ts @@ -2,16 +2,16 @@ import { DynamicModule, Module, Type } from '@nestjs/common'; import { APP_INTERCEPTOR } from '@nestjs/core'; import { AppController } from './app.controller'; -import { CacheInterceptor, CacheModule } from './cache'; -import { RedisModule } from './cache/redis'; -import { ConfigModule, SERVER_FLAVOR } from './config'; -import { EventModule } from './event'; -import { MetricsModule } from './metrics'; +import { CacheInterceptor, CacheModule } from './fundamentals/cache'; +import { ConfigModule } from './fundamentals/config'; +import { EventModule } from './fundamentals/event'; +import { MailModule } from './fundamentals/mailer'; +import { MetricsModule } from './fundamentals/metrics'; +import { PrismaModule } from './fundamentals/prisma'; +import { SessionModule } from './fundamentals/session'; +import { RateLimiterModule } from './fundamentals/throttler'; import { BusinessModules } from './modules'; import { AuthModule } from './modules/auth'; -import { PrismaModule } from './prisma'; -import { SessionModule } from './session'; -import { RateLimiterModule } from './throttler'; export const FunctionalityModules: Array = [ ConfigModule.forRoot(), @@ -22,13 +22,9 @@ export const FunctionalityModules: Array = [ SessionModule, RateLimiterModule, AuthModule, + MailModule, ]; -// better module registration logic -if (AFFiNE.redis.enabled) { - FunctionalityModules.push(RedisModule); -} - @Module({ providers: [ { @@ -37,6 +33,6 @@ if (AFFiNE.redis.enabled) { }, ], imports: [...FunctionalityModules, ...BusinessModules], - controllers: SERVER_FLAVOR === 'selfhosted' ? [] : [AppController], + controllers: [AppController], }) export class AppModule {} diff --git a/packages/backend/server/src/config/affine.env.ts b/packages/backend/server/src/config/affine.env.ts new file mode 100644 index 0000000000..be0b54f08e --- /dev/null +++ b/packages/backend/server/src/config/affine.env.ts @@ -0,0 +1,41 @@ +// Convenient way to map environment variables to config values. +AFFiNE.ENV_MAP = { + AFFINE_SERVER_PORT: ['port', 'int'], + AFFINE_SERVER_HOST: 'host', + AFFINE_SERVER_SUB_PATH: 'path', + AFFIHE_SERVER_HTTPS: ['https', 'boolean'], + AFFINE_ENV: 'affineEnv', + DATABASE_URL: 'db.url', + ENABLE_CAPTCHA: ['auth.captcha.enable', 'boolean'], + CAPTCHA_TURNSTILE_SECRET: ['auth.captcha.turnstile.secret', 'string'], + OAUTH_GOOGLE_ENABLED: ['auth.oauthProviders.google.enabled', 'boolean'], + OAUTH_GOOGLE_CLIENT_ID: 'auth.oauthProviders.google.clientId', + OAUTH_GOOGLE_CLIENT_SECRET: 'auth.oauthProviders.google.clientSecret', + OAUTH_GITHUB_ENABLED: ['auth.oauthProviders.github.enabled', 'boolean'], + OAUTH_GITHUB_CLIENT_ID: 'auth.oauthProviders.github.clientId', + OAUTH_GITHUB_CLIENT_SECRET: 'auth.oauthProviders.github.clientSecret', + OAUTH_EMAIL_LOGIN: 'auth.email.login', + OAUTH_EMAIL_SENDER: 'auth.email.sender', + OAUTH_EMAIL_SERVER: 'auth.email.server', + OAUTH_EMAIL_PORT: ['auth.email.port', 'int'], + OAUTH_EMAIL_PASSWORD: 'auth.email.password', + THROTTLE_TTL: ['rateLimiter.ttl', 'int'], + THROTTLE_LIMIT: ['rateLimiter.limit', 'int'], + REDIS_SERVER_ENABLED: ['redis.enabled', 'boolean'], + REDIS_SERVER_HOST: 'redis.host', + REDIS_SERVER_PORT: ['redis.port', 'int'], + REDIS_SERVER_USER: 'redis.username', + REDIS_SERVER_PASSWORD: 'redis.password', + REDIS_SERVER_DATABASE: ['redis.database', 'int'], + DOC_MERGE_INTERVAL: ['doc.manager.updatePollInterval', 'int'], + DOC_MERGE_USE_JWST_CODEC: [ + 'doc.manager.experimentalMergeWithJwstCodec', + 'boolean', + ], + ENABLE_LOCAL_EMAIL: ['auth.localEmail', 'boolean'], + STRIPE_API_KEY: 'payment.stripe.keys.APIKey', + STRIPE_WEBHOOK_KEY: 'payment.stripe.keys.webhookKey', + FEATURES_EARLY_ACCESS_PREVIEW: ['featureFlags.earlyAccessPreview', 'boolean'], +}; + +export default AFFiNE; diff --git a/packages/backend/server/src/affine.config.ts b/packages/backend/server/src/config/affine.ts similarity index 87% rename from packages/backend/server/src/affine.config.ts rename to packages/backend/server/src/config/affine.ts index 869b20467d..7ae037507f 100644 --- a/packages/backend/server/src/affine.config.ts +++ b/packages/backend/server/src/config/affine.ts @@ -1,11 +1,14 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ // Custom configurations - const env = process.env; const node = AFFiNE.node; -// TODO: may be separate config overring in `affine.[env].config`? +// TODO(@forehalo): detail explained +AFFiNE.host = 'localhost'; +AFFiNE.port = 3010; + if (node.prod) { + AFFiNE.host = '0.0.0.0'; // Storage if (env.R2_OBJECT_STORAGE_ACCOUNT_ID) { AFFiNE.storage.providers.r2 = { @@ -29,3 +32,5 @@ if (node.prod) { // Metrics AFFiNE.metrics.enabled = true; } + +export default AFFiNE; diff --git a/packages/backend/server/src/config/env.ts b/packages/backend/server/src/config/env.ts deleted file mode 100644 index 8d5b950fff..0000000000 --- a/packages/backend/server/src/config/env.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { set } from 'lodash-es'; - -import { type AFFiNEConfig, parseEnvValue } from './def'; - -export function applyEnvToConfig(rawConfig: AFFiNEConfig) { - for (const env in rawConfig.ENV_MAP) { - const config = rawConfig.ENV_MAP[env]; - const [path, value] = - typeof config === 'string' - ? [config, process.env[env]] - : [config[0], parseEnvValue(process.env[env], config[1])]; - - if (value !== undefined) { - set(rawConfig, path, value); - } - } -} diff --git a/packages/backend/server/src/constants.ts b/packages/backend/server/src/constants.ts deleted file mode 100644 index 5c0677a3ab..0000000000 --- a/packages/backend/server/src/constants.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const OPERATION_NAME = 'x-operation-name'; - -export const REQUEST_ID = 'x-request-id'; diff --git a/packages/backend/server/src/data/app.ts b/packages/backend/server/src/data/app.ts index 5e00fd214a..84c3027a3d 100644 --- a/packages/backend/server/src/data/app.ts +++ b/packages/backend/server/src/data/app.ts @@ -4,7 +4,7 @@ import { Logger, Module } from '@nestjs/common'; import { CommandFactory } from 'nest-commander'; import { AppModule as BusinessAppModule } from '../app'; -import { ConfigModule } from '../config'; +import { ConfigModule } from '../fundamentals/config'; import { CreateCommand, NameQuestion } from './commands/create'; import { RevertCommand, RunCommand } from './commands/run'; @@ -16,9 +16,6 @@ import { RevertCommand, RunCommand } from './commands/run'; enableUpdateAutoMerging: false, }, }, - metrics: { - enabled: false, - }, }), BusinessAppModule, ], diff --git a/packages/backend/server/src/data/commands/run.ts b/packages/backend/server/src/data/commands/run.ts index b8123cd8d9..136b7df0d7 100644 --- a/packages/backend/server/src/data/commands/run.ts +++ b/packages/backend/server/src/data/commands/run.ts @@ -4,15 +4,14 @@ import { fileURLToPath } from 'node:url'; import { Logger } from '@nestjs/common'; import { ModuleRef } from '@nestjs/core'; +import { PrismaClient } from '@prisma/client'; import { Command, CommandRunner } from 'nest-commander'; -import { PrismaService } from '../../prisma'; - interface Migration { file: string; name: string; - up: (db: PrismaService, injector: ModuleRef) => Promise; - down: (db: PrismaService, injector: ModuleRef) => Promise; + up: (db: PrismaClient, injector: ModuleRef) => Promise; + down: (db: PrismaClient, injector: ModuleRef) => Promise; } export async function collectMigrations(): Promise { @@ -48,7 +47,7 @@ export async function collectMigrations(): Promise { export class RunCommand extends CommandRunner { logger = new Logger(RunCommand.name); constructor( - private readonly db: PrismaService, + private readonly db: PrismaClient, private readonly injector: ModuleRef ) { super(); @@ -139,7 +138,7 @@ export class RevertCommand extends CommandRunner { logger = new Logger(RevertCommand.name); constructor( - private readonly db: PrismaService, + private readonly db: PrismaClient, private readonly injector: ModuleRef ) { super(); diff --git a/packages/backend/server/src/data/migrations/1698398506533-guid.ts b/packages/backend/server/src/data/migrations/1698398506533-guid.ts index e7e0262472..4a6393e286 100644 --- a/packages/backend/server/src/data/migrations/1698398506533-guid.ts +++ b/packages/backend/server/src/data/migrations/1698398506533-guid.ts @@ -1,7 +1,7 @@ import { PrismaClient } from '@prisma/client'; import { applyUpdate, Doc, encodeStateAsUpdate } from 'yjs'; -import { DocID } from '../../utils/doc'; +import { DocID } from '../../modules/utils/doc'; export class Guid1698398506533 { // do the migration diff --git a/packages/backend/server/src/cache/index.ts b/packages/backend/server/src/fundamentals/cache/index.ts similarity index 89% rename from packages/backend/server/src/cache/index.ts rename to packages/backend/server/src/fundamentals/cache/index.ts index 06c73367f2..2137f84cba 100644 --- a/packages/backend/server/src/cache/index.ts +++ b/packages/backend/server/src/fundamentals/cache/index.ts @@ -4,7 +4,7 @@ import { Redis } from 'ioredis'; import { SessionCache, ThrottlerCache } from './instances'; import { LocalCache } from './providers/cache'; import { RedisCache } from './providers/redis'; -import { CacheRedis, SessionRedis, ThrottlerRedis } from './redis'; +import { CacheRedis, RedisModule, SessionRedis, ThrottlerRedis } from './redis'; function makeCacheProvider(CacheToken: Type, RedisToken: Type): Provider { return { @@ -25,6 +25,7 @@ const ThrottlerCacheProvider = makeCacheProvider( @Global() @Module({ + imports: AFFiNE.redis.enabled ? [RedisModule] : [], providers: [CacheProvider, SessionCacheProvider, ThrottlerCacheProvider], exports: [CacheProvider, SessionCacheProvider, ThrottlerCacheProvider], }) diff --git a/packages/backend/server/src/cache/instances.ts b/packages/backend/server/src/fundamentals/cache/instances.ts similarity index 100% rename from packages/backend/server/src/cache/instances.ts rename to packages/backend/server/src/fundamentals/cache/instances.ts diff --git a/packages/backend/server/src/cache/interceptor.ts b/packages/backend/server/src/fundamentals/cache/interceptor.ts similarity index 100% rename from packages/backend/server/src/cache/interceptor.ts rename to packages/backend/server/src/fundamentals/cache/interceptor.ts diff --git a/packages/backend/server/src/cache/providers/cache.ts b/packages/backend/server/src/fundamentals/cache/providers/cache.ts similarity index 100% rename from packages/backend/server/src/cache/providers/cache.ts rename to packages/backend/server/src/fundamentals/cache/providers/cache.ts diff --git a/packages/backend/server/src/cache/providers/redis.ts b/packages/backend/server/src/fundamentals/cache/providers/redis.ts similarity index 100% rename from packages/backend/server/src/cache/providers/redis.ts rename to packages/backend/server/src/fundamentals/cache/providers/redis.ts diff --git a/packages/backend/server/src/cache/redis/index.ts b/packages/backend/server/src/fundamentals/cache/redis/index.ts similarity index 100% rename from packages/backend/server/src/cache/redis/index.ts rename to packages/backend/server/src/fundamentals/cache/redis/index.ts diff --git a/packages/backend/server/src/config/def.ts b/packages/backend/server/src/fundamentals/config/def.ts similarity index 90% rename from packages/backend/server/src/config/def.ts rename to packages/backend/server/src/fundamentals/config/def.ts index ceb871ee1f..ce8e80533e 100644 --- a/packages/backend/server/src/config/def.ts +++ b/packages/backend/server/src/fundamentals/config/def.ts @@ -1,6 +1,7 @@ import type { ApolloDriverConfig } from '@nestjs/apollo'; import type { LeafPaths } from '../utils/types'; +import { EnvConfigType } from './env'; import type { AFFiNEStorageConfig } from './storage'; declare global { @@ -18,8 +19,6 @@ export enum ExternalAccount { } export type ServerFlavor = 'allinone' | 'graphql' | 'sync' | 'selfhosted'; - -type EnvConfigType = 'string' | 'int' | 'float' | 'boolean'; type ConfigPaths = LeafPaths< Omit< AFFiNEConfig, @@ -31,40 +30,11 @@ type ConfigPaths = LeafPaths< | 'dev' | 'test' | 'deploy' + | 'node' >, '', '....' >; -/** - * parse number value from environment variables - */ -function int(value: string) { - const n = parseInt(value); - return Number.isNaN(n) ? undefined : n; -} - -function float(value: string) { - const n = parseFloat(value); - return Number.isNaN(n) ? undefined : n; -} - -function boolean(value: string) { - return value === '1' || value.toLowerCase() === 'true'; -} - -export function parseEnvValue(value: string | undefined, type?: EnvConfigType) { - if (value === undefined) { - return; - } - - return type === 'int' - ? int(value) - : type === 'float' - ? float(value) - : type === 'boolean' - ? boolean(value) - : value; -} /** * All Configurations that would control AFFiNE server behaviors @@ -80,6 +50,10 @@ export interface AFFiNEConfig { * System version */ readonly version: string; + /** + * Server flavor + */ + readonly flavor: ServerFlavor; /** * Deployment environment */ @@ -370,3 +344,5 @@ export interface AFFiNEConfig { } & import('stripe').Stripe.StripeConfig; }; } + +export * from './storage'; diff --git a/packages/backend/server/src/config/default.ts b/packages/backend/server/src/fundamentals/config/default.ts similarity index 63% rename from packages/backend/server/src/config/default.ts rename to packages/backend/server/src/fundamentals/config/default.ts index 94c8e72793..e16d3e464d 100644 --- a/packages/backend/server/src/config/default.ts +++ b/packages/backend/server/src/fundamentals/config/default.ts @@ -1,19 +1,15 @@ -/// +/// import { createPrivateKey, createPublicKey } from 'node:crypto'; import parse from 'parse-duration'; -import pkg from '../../package.json' assert { type: 'json' }; +import pkg from '../../../package.json' assert { type: 'json' }; import type { AFFiNEConfig, ServerFlavor } from './def'; -import { applyEnvToConfig } from './env'; import { getDefaultAFFiNEStorageConfig } from './storage'; -export const SERVER_FLAVOR = (process.env.SERVER_FLAVOR ?? - 'allinone') as ServerFlavor; - // Don't use this in production -export const examplePrivateKey = `-----BEGIN EC PRIVATE KEY----- +const examplePrivateKey = `-----BEGIN EC PRIVATE KEY----- MHcCAQEEIEtyAJLIULkphVhqXqxk4Nr8Ggty3XLwUJWBxzAWCWTMoAoGCCqGSM49 AwEHoUQDQgAEF3U/0wIeJ3jRKXeFKqQyBKlr9F7xaAUScRrAuSP33rajm3cdfihI 3JvMxVNsS2lE8PSGQrvDrJZaDo0L+Lq9Gg== @@ -50,50 +46,12 @@ const jwtKeyPair = (function () { export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => { let isHttps: boolean | null = null; + const flavor = (process.env.SERVER_FLAVOR ?? 'allinone') as ServerFlavor; const defaultConfig = { serverId: 'affine-nestjs-server', version: pkg.version, - ENV_MAP: { - AFFINE_SERVER_PORT: ['port', 'int'], - AFFINE_SERVER_HOST: 'host', - AFFINE_SERVER_SUB_PATH: 'path', - AFFIHE_SERVER_HTTPS: 'https', - AFFINE_ENV: 'affineEnv', - DATABASE_URL: 'db.url', - ENABLE_CAPTCHA: ['auth.captcha.enable', 'boolean'], - CAPTCHA_TURNSTILE_SECRET: ['auth.captcha.turnstile.secret', 'string'], - OAUTH_GOOGLE_ENABLED: ['auth.oauthProviders.google.enabled', 'boolean'], - OAUTH_GOOGLE_CLIENT_ID: 'auth.oauthProviders.google.clientId', - OAUTH_GOOGLE_CLIENT_SECRET: 'auth.oauthProviders.google.clientSecret', - OAUTH_GITHUB_ENABLED: ['auth.oauthProviders.github.enabled', 'boolean'], - OAUTH_GITHUB_CLIENT_ID: 'auth.oauthProviders.github.clientId', - OAUTH_GITHUB_CLIENT_SECRET: 'auth.oauthProviders.github.clientSecret', - OAUTH_EMAIL_LOGIN: 'auth.email.login', - OAUTH_EMAIL_SENDER: 'auth.email.sender', - OAUTH_EMAIL_SERVER: 'auth.email.server', - OAUTH_EMAIL_PORT: ['auth.email.port', 'int'], - OAUTH_EMAIL_PASSWORD: 'auth.email.password', - THROTTLE_TTL: ['rateLimiter.ttl', 'int'], - THROTTLE_LIMIT: ['rateLimiter.limit', 'int'], - REDIS_SERVER_ENABLED: ['redis.enabled', 'boolean'], - REDIS_SERVER_HOST: 'redis.host', - REDIS_SERVER_PORT: ['redis.port', 'int'], - REDIS_SERVER_USER: 'redis.username', - REDIS_SERVER_PASSWORD: 'redis.password', - REDIS_SERVER_DATABASE: ['redis.database', 'int'], - DOC_MERGE_INTERVAL: ['doc.manager.updatePollInterval', 'int'], - DOC_MERGE_USE_JWST_CODEC: [ - 'doc.manager.experimentalMergeWithJwstCodec', - 'boolean', - ], - ENABLE_LOCAL_EMAIL: ['auth.localEmail', 'boolean'], - STRIPE_API_KEY: 'payment.stripe.keys.APIKey', - STRIPE_WEBHOOK_KEY: 'payment.stripe.keys.webhookKey', - FEATURES_EARLY_ACCESS_PREVIEW: [ - 'featureFlags.earlyAccessPreview', - 'boolean', - ], - } satisfies AFFiNEConfig['ENV_MAP'], + flavor, + ENV_MAP: {}, affineEnv: 'dev', get affine() { const env = this.affineEnv; @@ -194,7 +152,7 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => { }, doc: { manager: { - enableUpdateAutoMerging: SERVER_FLAVOR !== 'sync', + enableUpdateAutoMerging: flavor !== 'sync', updatePollInterval: 3000, experimentalMergeWithJwstCodec: false, }, @@ -216,7 +174,5 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => { }, } satisfies AFFiNEConfig; - applyEnvToConfig(defaultConfig); - return defaultConfig; }; diff --git a/packages/backend/server/src/fundamentals/config/env.ts b/packages/backend/server/src/fundamentals/config/env.ts new file mode 100644 index 0000000000..50554bc462 --- /dev/null +++ b/packages/backend/server/src/fundamentals/config/env.ts @@ -0,0 +1,50 @@ +import { set } from 'lodash-es'; + +import type { AFFiNEConfig } from './def'; + +export type EnvConfigType = 'string' | 'int' | 'float' | 'boolean'; +/** + * parse number value from environment variables + */ +function int(value: string) { + const n = parseInt(value); + return Number.isNaN(n) ? undefined : n; +} + +function float(value: string) { + const n = parseFloat(value); + return Number.isNaN(n) ? undefined : n; +} + +function boolean(value: string) { + return value === '1' || value.toLowerCase() === 'true'; +} + +const envParsers: Record unknown> = { + int, + float, + boolean, + string: value => value, +}; + +export function parseEnvValue(value: string | undefined, type: EnvConfigType) { + if (value === undefined) { + return; + } + + return envParsers[type](value); +} + +export function applyEnvToConfig(rawConfig: AFFiNEConfig) { + for (const env in rawConfig.ENV_MAP) { + const config = rawConfig.ENV_MAP[env]; + const [path, value] = + typeof config === 'string' + ? [config, parseEnvValue(process.env[env], 'string')] + : [config[0], parseEnvValue(process.env[env], config[1] ?? 'string')]; + + if (value !== undefined) { + set(rawConfig, path, value); + } + } +} diff --git a/packages/backend/server/src/fundamentals/config/index.ts b/packages/backend/server/src/fundamentals/config/index.ts new file mode 100644 index 0000000000..34fadc0421 --- /dev/null +++ b/packages/backend/server/src/fundamentals/config/index.ts @@ -0,0 +1,6 @@ +// eslint-disable-next-line simple-import-sort/imports + +export * from './def'; +export * from './default'; +export { applyEnvToConfig, parseEnvValue } from './env'; +export * from './module'; diff --git a/packages/backend/server/src/config/index.ts b/packages/backend/server/src/fundamentals/config/module.ts similarity index 68% rename from packages/backend/server/src/config/index.ts rename to packages/backend/server/src/fundamentals/config/module.ts index 433221764e..8af40f23e9 100644 --- a/packages/backend/server/src/config/index.ts +++ b/packages/backend/server/src/fundamentals/config/module.ts @@ -1,24 +1,12 @@ -// eslint-disable-next-line simple-import-sort/imports -import type { DynamicModule, FactoryProvider } from '@nestjs/common'; +import { DynamicModule, FactoryProvider } from '@nestjs/common'; import { merge } from 'lodash-es'; -import type { DeepPartial } from '../utils/types'; -import type { AFFiNEConfig } from './def'; - -type ConstructorOf = { - new (): T; -}; - -function ApplyType(): ConstructorOf { - // @ts-expect-error used to fake the type of config - return class Inner implements T { - constructor() {} - }; -} +import { ApplyType, DeepPartial } from '../utils/types'; +import { AFFiNEConfig } from './def'; /** - * usage: - * ``` + * @example + * * import { Config } from '@affine/server' * * class TestConfig { @@ -27,7 +15,6 @@ function ApplyType(): ConstructorOf { * return this.config.env * } * } - * ``` */ export class Config extends ApplyType() {} @@ -69,7 +56,3 @@ export class ConfigModule { }; }; } - -export type { AFFiNEConfig } from './def'; -export { SERVER_FLAVOR } from './default'; -export * from './storage'; diff --git a/packages/backend/server/src/config/storage/index.ts b/packages/backend/server/src/fundamentals/config/storage/index.ts similarity index 100% rename from packages/backend/server/src/config/storage/index.ts rename to packages/backend/server/src/fundamentals/config/storage/index.ts diff --git a/packages/backend/server/src/event/events.ts b/packages/backend/server/src/fundamentals/event/events.ts similarity index 100% rename from packages/backend/server/src/event/events.ts rename to packages/backend/server/src/fundamentals/event/events.ts diff --git a/packages/backend/server/src/event/index.ts b/packages/backend/server/src/fundamentals/event/index.ts similarity index 100% rename from packages/backend/server/src/event/index.ts rename to packages/backend/server/src/fundamentals/event/index.ts diff --git a/packages/backend/server/src/event/types.ts b/packages/backend/server/src/fundamentals/event/types.ts similarity index 100% rename from packages/backend/server/src/event/types.ts rename to packages/backend/server/src/fundamentals/event/types.ts diff --git a/packages/backend/server/src/graphql.module.ts b/packages/backend/server/src/fundamentals/graphql/index.ts similarity index 72% rename from packages/backend/server/src/graphql.module.ts rename to packages/backend/server/src/fundamentals/graphql/index.ts index 71610e2235..5f1b055638 100644 --- a/packages/backend/server/src/graphql.module.ts +++ b/packages/backend/server/src/fundamentals/graphql/index.ts @@ -6,8 +6,8 @@ import { Request, Response } from 'express'; import { join } from 'path'; import { fileURLToPath } from 'url'; -import { Config } from './config'; -import { GQLLoggerPlugin } from './graphql/logger-plugin'; +import { Config } from '../config'; +import { GQLLoggerPlugin } from './logger-plugin'; @Global() @Module({ @@ -21,12 +21,12 @@ import { GQLLoggerPlugin } from './graphql/logger-plugin'; csrfPrevention: { requestHeaders: ['content-type'], }, - autoSchemaFile: config.node.test - ? join( - fileURLToPath(import.meta.url), - '../../node_modules/.cache/schema.gql' - ) - : join(fileURLToPath(import.meta.url), '..', 'schema.gql'), + autoSchemaFile: join( + fileURLToPath(import.meta.url), + config.node.test + ? '../../../../node_modules/.cache/schema.gql' + : '../../../schema.gql' + ), context: ({ req, res }: { req: Request; res: Response }) => ({ req, res, diff --git a/packages/backend/server/src/graphql/logger-plugin.ts b/packages/backend/server/src/fundamentals/graphql/logger-plugin.ts similarity index 79% rename from packages/backend/server/src/graphql/logger-plugin.ts rename to packages/backend/server/src/fundamentals/graphql/logger-plugin.ts index 9028ce722a..628c6ce42a 100644 --- a/packages/backend/server/src/graphql/logger-plugin.ts +++ b/packages/backend/server/src/fundamentals/graphql/logger-plugin.ts @@ -8,17 +8,22 @@ import { Logger } from '@nestjs/common'; import { Response } from 'express'; import { metrics } from '../metrics/metrics'; -import { ReqContext } from '../types'; + +export interface RequestContext { + req: Express.Request & { + res: Express.Response; + }; +} @Plugin() export class GQLLoggerPlugin implements ApolloServerPlugin { protected logger = new Logger(GQLLoggerPlugin.name); requestDidStart( - reqContext: GraphQLRequestContext - ): Promise>> { - const res = reqContext.contextValue.req.res as Response; - const operation = reqContext.request.operationName; + ctx: GraphQLRequestContext + ): Promise>> { + const res = ctx.contextValue.req.res as Response; + const operation = ctx.request.operationName; metrics.gql.counter('query_counter').add(1, { operation }); const start = Date.now(); diff --git a/packages/backend/server/src/fundamentals/index.ts b/packages/backend/server/src/fundamentals/index.ts new file mode 100644 index 0000000000..35b18decc1 --- /dev/null +++ b/packages/backend/server/src/fundamentals/index.ts @@ -0,0 +1,20 @@ +export { Cache, CacheInterceptor, MakeCache, PreventCache } from './cache'; +export { + applyEnvToConfig, + Config, + getDefaultAFFiNEStorageConfig, +} from './config'; +export { EventEmitter, type EventPayload, OnEvent } from './event'; +export { MailService } from './mailer'; +export { CallCounter, CallTimer, metrics } from './metrics'; +export { PrismaService } from './prisma'; +export { SessionService } from './session'; +export * from './storage'; +export { AuthThrottlerGuard, CloudThrottlerGuard, Throttle } from './throttler'; +export { + getRequestFromHost, + getRequestResponseFromContext, + getRequestResponseFromHost, +} from './utils/request'; +export type * from './utils/types'; +export { RedisIoAdapter } from './websocket'; diff --git a/packages/backend/server/src/fundamentals/mailer/index.ts b/packages/backend/server/src/fundamentals/mailer/index.ts new file mode 100644 index 0000000000..e990f6c489 --- /dev/null +++ b/packages/backend/server/src/fundamentals/mailer/index.ts @@ -0,0 +1,12 @@ +import { Global, Module } from '@nestjs/common'; + +import { MailService } from './mail.service'; +import { MAILER } from './mailer'; + +@Global() +@Module({ + providers: [MAILER, MailService], + exports: [MailService], +}) +export class MailModule {} +export { MailService }; diff --git a/packages/backend/server/src/modules/auth/mailer/mail.service.ts b/packages/backend/server/src/fundamentals/mailer/mail.service.ts similarity index 99% rename from packages/backend/server/src/modules/auth/mailer/mail.service.ts rename to packages/backend/server/src/fundamentals/mailer/mail.service.ts index 702d2fae3e..66241e90c7 100644 --- a/packages/backend/server/src/modules/auth/mailer/mail.service.ts +++ b/packages/backend/server/src/fundamentals/mailer/mail.service.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; -import { Config } from '../../../config'; +import { Config } from '../config'; import { MAILER_SERVICE, type MailerService, diff --git a/packages/backend/server/src/modules/auth/mailer/mailer.ts b/packages/backend/server/src/fundamentals/mailer/mailer.ts similarity index 95% rename from packages/backend/server/src/modules/auth/mailer/mailer.ts rename to packages/backend/server/src/fundamentals/mailer/mailer.ts index d1c8f65c3a..ba02cc8887 100644 --- a/packages/backend/server/src/modules/auth/mailer/mailer.ts +++ b/packages/backend/server/src/fundamentals/mailer/mailer.ts @@ -2,7 +2,7 @@ import { FactoryProvider } from '@nestjs/common'; import { createTransport, Transporter } from 'nodemailer'; import SMTPTransport from 'nodemailer/lib/smtp-transport'; -import { Config } from '../../../config'; +import { Config } from '../config'; export const MAILER_SERVICE = Symbol('MAILER_SERVICE'); diff --git a/packages/backend/server/src/modules/auth/mailer/template.ts b/packages/backend/server/src/fundamentals/mailer/template.ts similarity index 100% rename from packages/backend/server/src/modules/auth/mailer/template.ts rename to packages/backend/server/src/fundamentals/mailer/template.ts diff --git a/packages/backend/server/src/metrics/index.ts b/packages/backend/server/src/fundamentals/metrics/index.ts similarity index 90% rename from packages/backend/server/src/metrics/index.ts rename to packages/backend/server/src/fundamentals/metrics/index.ts index 9ca470b582..dc2b481daa 100644 --- a/packages/backend/server/src/metrics/index.ts +++ b/packages/backend/server/src/fundamentals/metrics/index.ts @@ -1,8 +1,7 @@ import { Global, Module, OnModuleDestroy, OnModuleInit } from '@nestjs/common'; import { NodeSDK } from '@opentelemetry/sdk-node'; -import { Config } from '../config'; -import { parseEnvValue } from '../config/def'; +import { Config, parseEnvValue } from '../config'; import { createSDK, registerCustomMetrics } from './opentelemetry'; @Global() diff --git a/packages/backend/server/src/metrics/metrics.ts b/packages/backend/server/src/fundamentals/metrics/metrics.ts similarity index 100% rename from packages/backend/server/src/metrics/metrics.ts rename to packages/backend/server/src/fundamentals/metrics/metrics.ts diff --git a/packages/backend/server/src/metrics/opentelemetry.ts b/packages/backend/server/src/fundamentals/metrics/opentelemetry.ts similarity index 100% rename from packages/backend/server/src/metrics/opentelemetry.ts rename to packages/backend/server/src/fundamentals/metrics/opentelemetry.ts diff --git a/packages/backend/server/src/metrics/prisma.ts b/packages/backend/server/src/fundamentals/metrics/prisma.ts similarity index 100% rename from packages/backend/server/src/metrics/prisma.ts rename to packages/backend/server/src/fundamentals/metrics/prisma.ts diff --git a/packages/backend/server/src/metrics/utils.ts b/packages/backend/server/src/fundamentals/metrics/utils.ts similarity index 100% rename from packages/backend/server/src/metrics/utils.ts rename to packages/backend/server/src/fundamentals/metrics/utils.ts diff --git a/packages/backend/server/src/prisma/index.ts b/packages/backend/server/src/fundamentals/prisma/index.ts similarity index 100% rename from packages/backend/server/src/prisma/index.ts rename to packages/backend/server/src/fundamentals/prisma/index.ts diff --git a/packages/backend/server/src/prisma/service.ts b/packages/backend/server/src/fundamentals/prisma/service.ts similarity index 100% rename from packages/backend/server/src/prisma/service.ts rename to packages/backend/server/src/fundamentals/prisma/service.ts diff --git a/packages/backend/server/src/session.ts b/packages/backend/server/src/fundamentals/session/index.ts similarity index 95% rename from packages/backend/server/src/session.ts rename to packages/backend/server/src/fundamentals/session/index.ts index 878bf0e821..a185452abb 100644 --- a/packages/backend/server/src/session.ts +++ b/packages/backend/server/src/fundamentals/session/index.ts @@ -1,6 +1,6 @@ import { Global, Injectable, Module } from '@nestjs/common'; -import { SessionCache } from './cache/instances'; +import { SessionCache } from '../cache'; @Injectable() export class SessionService { diff --git a/packages/backend/server/src/modules/storage/__tests__/fs.spec.ts b/packages/backend/server/src/fundamentals/storage/__tests__/fs.spec.ts similarity index 100% rename from packages/backend/server/src/modules/storage/__tests__/fs.spec.ts rename to packages/backend/server/src/fundamentals/storage/__tests__/fs.spec.ts diff --git a/packages/backend/server/src/storage/index.ts b/packages/backend/server/src/fundamentals/storage/index.ts similarity index 69% rename from packages/backend/server/src/storage/index.ts rename to packages/backend/server/src/fundamentals/storage/index.ts index 8d2fc87b18..d2ecdf9284 100644 --- a/packages/backend/server/src/storage/index.ts +++ b/packages/backend/server/src/fundamentals/storage/index.ts @@ -9,10 +9,10 @@ try { const require = createRequire(import.meta.url); storageModule = process.arch === 'arm64' - ? require('../../storage.arm64.node') + ? require('../.././storage.arm64.node') : process.arch === 'arm' - ? require('../../storage.armv7.node') - : require('../../storage.node'); + ? require('../../../storage.armv7.node') + : require('../../../storage.node'); } export { storageModule as OctoBaseStorageModule }; @@ -32,3 +32,14 @@ export const mintChallengeResponse = async (resource: string, bits: number) => { if (!resource) return null; return storageModule.mintChallengeResponse(resource, bits); }; + +export type { + BlobInputType, + BlobOutputType, + GetObjectMetadata, + ListObjectsMetadata, + PutObjectMetadata, + StorageProvider, +} from './providers'; +export { createStorageProvider } from './providers'; +export { toBuffer } from './providers/utils'; diff --git a/packages/backend/server/src/modules/storage/providers/fs.ts b/packages/backend/server/src/fundamentals/storage/providers/fs.ts similarity index 99% rename from packages/backend/server/src/modules/storage/providers/fs.ts rename to packages/backend/server/src/fundamentals/storage/providers/fs.ts index 3ad735d2e4..46dbe0cdea 100644 --- a/packages/backend/server/src/modules/storage/providers/fs.ts +++ b/packages/backend/server/src/fundamentals/storage/providers/fs.ts @@ -15,7 +15,7 @@ import { join, parse, resolve } from 'node:path'; import { Logger } from '@nestjs/common'; import { Readable } from 'stream'; -import { FsStorageConfig } from '../../../config/storage'; +import { FsStorageConfig } from '../../config/storage'; import { BlobInputType, GetObjectMetadata, diff --git a/packages/backend/server/src/modules/storage/providers/index.ts b/packages/backend/server/src/fundamentals/storage/providers/index.ts similarity index 93% rename from packages/backend/server/src/modules/storage/providers/index.ts rename to packages/backend/server/src/fundamentals/storage/providers/index.ts index bfcc51cda2..62c6d81d01 100644 --- a/packages/backend/server/src/modules/storage/providers/index.ts +++ b/packages/backend/server/src/fundamentals/storage/providers/index.ts @@ -1,4 +1,4 @@ -import { AFFiNEStorageConfig, Storages } from '../../../config/storage'; +import { AFFiNEStorageConfig, Storages } from '../../config/storage'; import { FsStorageProvider } from './fs'; import type { StorageProvider } from './provider'; import { R2StorageProvider } from './r2'; diff --git a/packages/backend/server/src/modules/storage/providers/provider.ts b/packages/backend/server/src/fundamentals/storage/providers/provider.ts similarity index 94% rename from packages/backend/server/src/modules/storage/providers/provider.ts rename to packages/backend/server/src/fundamentals/storage/providers/provider.ts index 6e83189184..0c9114b657 100644 --- a/packages/backend/server/src/modules/storage/providers/provider.ts +++ b/packages/backend/server/src/fundamentals/storage/providers/provider.ts @@ -1,6 +1,6 @@ import type { Readable } from 'node:stream'; -import { StorageProviderType } from '../../../config'; +import { StorageProviderType } from '../../config'; export interface GetObjectMetadata { /** diff --git a/packages/backend/server/src/modules/storage/providers/r2.ts b/packages/backend/server/src/fundamentals/storage/providers/r2.ts similarity index 89% rename from packages/backend/server/src/modules/storage/providers/r2.ts rename to packages/backend/server/src/fundamentals/storage/providers/r2.ts index 9d9f832bec..6e2b9cd5b0 100644 --- a/packages/backend/server/src/modules/storage/providers/r2.ts +++ b/packages/backend/server/src/fundamentals/storage/providers/r2.ts @@ -1,6 +1,6 @@ import { Logger } from '@nestjs/common'; -import { R2StorageConfig } from '../../../config/storage'; +import { R2StorageConfig } from '../../config/storage'; import { S3StorageProvider } from './s3'; export class R2StorageProvider extends S3StorageProvider { diff --git a/packages/backend/server/src/modules/storage/providers/s3.ts b/packages/backend/server/src/fundamentals/storage/providers/s3.ts similarity index 98% rename from packages/backend/server/src/modules/storage/providers/s3.ts rename to packages/backend/server/src/fundamentals/storage/providers/s3.ts index f1f811998d..09d8d83cd3 100644 --- a/packages/backend/server/src/modules/storage/providers/s3.ts +++ b/packages/backend/server/src/fundamentals/storage/providers/s3.ts @@ -11,7 +11,7 @@ import { } from '@aws-sdk/client-s3'; import { Logger } from '@nestjs/common'; -import { S3StorageConfig } from '../../../config/storage'; +import { S3StorageConfig } from '../../config/storage'; import { BlobInputType, GetObjectMetadata, diff --git a/packages/backend/server/src/modules/storage/providers/utils.ts b/packages/backend/server/src/fundamentals/storage/providers/utils.ts similarity index 100% rename from packages/backend/server/src/modules/storage/providers/utils.ts rename to packages/backend/server/src/fundamentals/storage/providers/utils.ts diff --git a/packages/backend/server/src/throttler.ts b/packages/backend/server/src/fundamentals/throttler/index.ts similarity index 93% rename from packages/backend/server/src/throttler.ts rename to packages/backend/server/src/fundamentals/throttler/index.ts index a94fe9355b..a7c4ee4ee6 100644 --- a/packages/backend/server/src/throttler.ts +++ b/packages/backend/server/src/fundamentals/throttler/index.ts @@ -9,9 +9,9 @@ import { } from '@nestjs/throttler'; import { ThrottlerStorageRedisService } from 'nestjs-throttler-storage-redis'; -import { ThrottlerCache } from './cache'; -import { Config } from './config'; -import { getRequestResponseFromContext } from './utils/nestjs'; +import { ThrottlerCache } from '../cache'; +import { Config } from '../config'; +import { getRequestResponseFromContext } from '../utils/request'; @Injectable() class CustomOptionsFactory implements ThrottlerOptionsFactory { diff --git a/packages/backend/server/src/utils/nestjs.ts b/packages/backend/server/src/fundamentals/utils/request.ts similarity index 100% rename from packages/backend/server/src/utils/nestjs.ts rename to packages/backend/server/src/fundamentals/utils/request.ts diff --git a/packages/backend/server/src/utils/types.ts b/packages/backend/server/src/fundamentals/utils/types.ts similarity index 71% rename from packages/backend/server/src/utils/types.ts rename to packages/backend/server/src/fundamentals/utils/types.ts index 10079af2b5..09c96b9d77 100644 --- a/packages/backend/server/src/utils/types.ts +++ b/packages/backend/server/src/fundamentals/utils/types.ts @@ -1,3 +1,16 @@ +import { Readable } from 'node:stream'; + +export type ConstructorOf = { + new (): T; +}; + +export function ApplyType(): ConstructorOf { + // @ts-expect-error used to fake the type of config + return class Inner implements T { + constructor() {} + }; +} + export type DeepPartial = T extends Array ? DeepPartial[] : T extends ReadonlyArray @@ -40,3 +53,10 @@ export type LeafPaths< : never; }[keyof T] : never; + +export interface FileUpload { + filename: string; + mimetype: string; + encoding: string; + createReadStream: () => Readable; +} diff --git a/packages/backend/server/src/fundamentals/websocket/index.ts b/packages/backend/server/src/fundamentals/websocket/index.ts new file mode 100644 index 0000000000..f772e0ea2c --- /dev/null +++ b/packages/backend/server/src/fundamentals/websocket/index.ts @@ -0,0 +1 @@ +export { RedisIoAdapter } from './redis-adapter'; diff --git a/packages/backend/server/src/modules/sync/redis-adapter.ts b/packages/backend/server/src/fundamentals/websocket/redis-adapter.ts similarity index 100% rename from packages/backend/server/src/modules/sync/redis-adapter.ts rename to packages/backend/server/src/fundamentals/websocket/redis-adapter.ts diff --git a/packages/backend/server/src/index.ts b/packages/backend/server/src/index.ts index 6333e1798d..60a8ce3fb8 100644 --- a/packages/backend/server/src/index.ts +++ b/packages/backend/server/src/index.ts @@ -9,21 +9,16 @@ import cookieParser from 'cookie-parser'; import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs'; import { AppModule } from './app'; -import { Config } from './config'; +import { RedisIoAdapter } from './fundamentals'; +import { CacheRedis } from './fundamentals/cache/redis'; import { ExceptionLogger } from './middleware/exception-logger'; import { serverTimingAndCache } from './middleware/timing'; -import { RedisIoAdapter } from './modules/sync/redis-adapter'; -import { CacheRedis } from './cache/redis'; -const { NODE_ENV, AFFINE_ENV } = process.env; const app = await NestFactory.create(AppModule, { cors: true, rawBody: true, bodyParser: true, - logger: - NODE_ENV !== 'production' || AFFINE_ENV !== 'production' - ? ['verbose'] - : ['log'], + logger: AFFiNE.affine.stable ? ['log'] : ['verbose'], }); app.use(serverTimingAndCache); @@ -39,18 +34,16 @@ app.use( app.useGlobalFilters(new ExceptionLogger()); app.use(cookieParser()); -const config = app.get(Config); - -const host = config.node.prod ? '0.0.0.0' : 'localhost'; -const port = config.port ?? 3010; - -if (!config.node.test && config.redis.enabled) { +if (AFFiNE.redis.enabled) { const redis = app.get(CacheRedis, { strict: false }); const redisIoAdapter = new RedisIoAdapter(app); await redisIoAdapter.connectToRedis(redis); app.useWebSocketAdapter(redisIoAdapter); } -await app.listen(port, host); +await app.listen(AFFiNE.port, AFFiNE.host); -console.log(`Listening on http://${host}:${port}`); +console.log( + `AFFiNE Server has been started on http://${AFFiNE.host}:${AFFiNE.port}.` +); +console.log(`And the public server should be recognized as ${AFFiNE.baseUrl}`); diff --git a/packages/backend/server/src/middleware/exception-logger.ts b/packages/backend/server/src/middleware/exception-logger.ts index 4e4c97d3c8..8630c118d9 100644 --- a/packages/backend/server/src/middleware/exception-logger.ts +++ b/packages/backend/server/src/middleware/exception-logger.ts @@ -9,9 +9,10 @@ import { import { GqlContextType } from '@nestjs/graphql'; import { Request, Response } from 'express'; -import { REQUEST_ID } from '../constants'; const TrivialExceptions = [NotFoundException]; +export const REQUEST_ID_HEADER = 'x-request-id'; + @Catch() export class ExceptionLogger implements ExceptionFilter { private readonly logger = new Logger('ExceptionLogger'); @@ -21,7 +22,7 @@ export class ExceptionLogger implements ExceptionFilter { const ctx = host.switchToHttp(); const request = ctx.getRequest(); - const requestId = request?.header(REQUEST_ID); + const requestId = request?.header(REQUEST_ID_HEADER); const shouldVerboseLog = !TrivialExceptions.some( e => exception instanceof e diff --git a/packages/backend/server/src/modules/auth/guard.ts b/packages/backend/server/src/modules/auth/guard.ts index 7e311159d2..eab1577677 100644 --- a/packages/backend/server/src/modules/auth/guard.ts +++ b/packages/backend/server/src/modules/auth/guard.ts @@ -10,8 +10,10 @@ import { Reflector } from '@nestjs/core'; import type { NextAuthOptions } from 'next-auth'; import { AuthHandler } from 'next-auth/core'; -import { PrismaService } from '../../prisma'; -import { getRequestResponseFromContext } from '../../utils/nestjs'; +import { + getRequestResponseFromContext, + PrismaService, +} from '../../fundamentals'; import { NextAuthOptionsProvide } from './next-auth-options'; import { AuthService } from './service'; diff --git a/packages/backend/server/src/modules/auth/index.ts b/packages/backend/server/src/modules/auth/index.ts index 95dc5405c7..b5b72ab31e 100644 --- a/packages/backend/server/src/modules/auth/index.ts +++ b/packages/backend/server/src/modules/auth/index.ts @@ -1,7 +1,5 @@ import { Global, Module } from '@nestjs/common'; -import { SessionModule } from '../../session'; -import { MAILER, MailService } from './mailer'; import { NextAuthController } from './next-auth.controller'; import { NextAuthOptionsProvider } from './next-auth-options'; import { AuthResolver } from './resolver'; @@ -9,18 +7,12 @@ import { AuthService } from './service'; @Global() @Module({ - imports: [SessionModule], - providers: [ - AuthService, - AuthResolver, - NextAuthOptionsProvider, - MAILER, - MailService, - ], - exports: [AuthService, NextAuthOptionsProvider, MailService], + providers: [AuthService, AuthResolver, NextAuthOptionsProvider], + exports: [AuthService, NextAuthOptionsProvider], controllers: [NextAuthController], }) export class AuthModule {} export * from './guard'; export { TokenType } from './resolver'; +export { AuthService }; diff --git a/packages/backend/server/src/modules/auth/mailer/index.ts b/packages/backend/server/src/modules/auth/mailer/index.ts deleted file mode 100644 index 9c53bf57ca..0000000000 --- a/packages/backend/server/src/modules/auth/mailer/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { MailService } from './mail.service'; -export { MAILER } from './mailer'; diff --git a/packages/backend/server/src/modules/auth/next-auth-options.ts b/packages/backend/server/src/modules/auth/next-auth-options.ts index 841f244249..261ad7d8ec 100644 --- a/packages/backend/server/src/modules/auth/next-auth-options.ts +++ b/packages/backend/server/src/modules/auth/next-auth-options.ts @@ -8,12 +8,14 @@ import Email from 'next-auth/providers/email'; import Github from 'next-auth/providers/github'; import Google from 'next-auth/providers/google'; -import { Config } from '../../config'; -import { PrismaService } from '../../prisma'; -import { SessionService } from '../../session'; +import { + Config, + MailService, + PrismaService, + SessionService, +} from '../../fundamentals'; import { FeatureType } from '../features'; import { Quota_FreePlanV1 } from '../quota'; -import { MailService } from './mailer'; import { decode, encode, diff --git a/packages/backend/server/src/modules/auth/next-auth.controller.ts b/packages/backend/server/src/modules/auth/next-auth.controller.ts index fee533ecea..a58d89c117 100644 --- a/packages/backend/server/src/modules/auth/next-auth.controller.ts +++ b/packages/backend/server/src/modules/auth/next-auth.controller.ts @@ -22,11 +22,14 @@ import { nanoid } from 'nanoid'; import type { AuthAction, CookieOption, NextAuthOptions } from 'next-auth'; import { AuthHandler } from 'next-auth/core'; -import { Config } from '../../config'; -import { metrics } from '../../metrics'; -import { PrismaService } from '../../prisma/service'; -import { SessionService } from '../../session'; -import { AuthThrottlerGuard, Throttle } from '../../throttler'; +import { + AuthThrottlerGuard, + Config, + metrics, + PrismaService, + SessionService, + Throttle, +} from '../../fundamentals'; import { NextAuthOptionsProvide } from './next-auth-options'; import { AuthService } from './service'; diff --git a/packages/backend/server/src/modules/auth/resolver.ts b/packages/backend/server/src/modules/auth/resolver.ts index aac7b4e673..8ab9ef089d 100644 --- a/packages/backend/server/src/modules/auth/resolver.ts +++ b/packages/backend/server/src/modules/auth/resolver.ts @@ -16,9 +16,12 @@ import { import type { Request } from 'express'; import { nanoid } from 'nanoid'; -import { Config } from '../../config'; -import { SessionService } from '../../session'; -import { CloudThrottlerGuard, Throttle } from '../../throttler'; +import { + CloudThrottlerGuard, + Config, + SessionService, + Throttle, +} from '../../fundamentals'; import { UserType } from '../users'; import { Auth, CurrentUser } from './guard'; import { AuthService } from './service'; diff --git a/packages/backend/server/src/modules/auth/service.ts b/packages/backend/server/src/modules/auth/service.ts index 59239cb799..306ff4e090 100644 --- a/packages/backend/server/src/modules/auth/service.ts +++ b/packages/backend/server/src/modules/auth/service.ts @@ -11,11 +11,13 @@ import { Algorithm, sign, verify as jwtVerify } from '@node-rs/jsonwebtoken'; import type { User } from '@prisma/client'; import { nanoid } from 'nanoid'; -import { Config } from '../../config'; -import { PrismaService } from '../../prisma'; -import { verifyChallengeResponse } from '../../storage'; +import { + Config, + MailService, + PrismaService, + verifyChallengeResponse, +} from '../../fundamentals'; import { Quota_FreePlanV1 } from '../quota'; -import { MailService } from './mailer'; export type UserClaim = Pick< User, @@ -192,6 +194,7 @@ export class AuthService { name, email, password: hashedPassword, + // TODO(@forehalo): handle in event system features: { create: { reason: 'created by api sign up', diff --git a/packages/backend/server/src/modules/auth/utils/jwt.ts b/packages/backend/server/src/modules/auth/utils/jwt.ts index 82b9bd42c9..929a26f1ad 100644 --- a/packages/backend/server/src/modules/auth/utils/jwt.ts +++ b/packages/backend/server/src/modules/auth/utils/jwt.ts @@ -4,8 +4,7 @@ import { BadRequestException } from '@nestjs/common'; import { Algorithm, sign, verify as jwtVerify } from '@node-rs/jsonwebtoken'; import { JWT } from 'next-auth/jwt'; -import { Config } from '../../../config'; -import { PrismaService } from '../../../prisma'; +import { Config, PrismaService } from '../../../fundamentals'; import { getUtcTimestamp, UserClaim } from '../service'; export const jwtEncode = async ( diff --git a/packages/backend/server/src/modules/auth/utils/send-mail.ts b/packages/backend/server/src/modules/auth/utils/send-mail.ts index c2cf89b578..9cc349bd53 100644 --- a/packages/backend/server/src/modules/auth/utils/send-mail.ts +++ b/packages/backend/server/src/modules/auth/utils/send-mail.ts @@ -2,9 +2,7 @@ import { Logger } from '@nestjs/common'; import { nanoid } from 'nanoid'; import type { SendVerificationRequestParams } from 'next-auth/providers/email'; -import { Config } from '../../../config'; -import { SessionService } from '../../../session'; -import { MailService } from '../mailer'; +import { Config, MailService, SessionService } from '../../../fundamentals'; export async function sendVerificationRequest( config: Config, diff --git a/packages/backend/server/src/modules/config.ts b/packages/backend/server/src/modules/config.ts index 7ebd5b5a4c..7286e496d8 100644 --- a/packages/backend/server/src/modules/config.ts +++ b/packages/backend/server/src/modules/config.ts @@ -1,8 +1,6 @@ import { Module } from '@nestjs/common'; import { Field, ObjectType, Query } from '@nestjs/graphql'; -import { SERVER_FLAVOR } from '../config'; - @ObjectType() export class ServerConfigType { @Field({ description: 'server version' }) @@ -22,7 +20,7 @@ export class ServerConfigResolver { serverConfig(): ServerConfigType { return { version: AFFiNE.version, - flavor: SERVER_FLAVOR, + flavor: AFFiNE.flavor, baseUrl: AFFiNE.baseUrl, }; } diff --git a/packages/backend/server/src/modules/doc/history.ts b/packages/backend/server/src/modules/doc/history.ts index ab6a5dcc7d..303b6fd56e 100644 --- a/packages/backend/server/src/modules/doc/history.ts +++ b/packages/backend/server/src/modules/doc/history.ts @@ -3,10 +3,13 @@ import { isDeepStrictEqual } from 'node:util'; import { Injectable, Logger } from '@nestjs/common'; import { Cron, CronExpression } from '@nestjs/schedule'; -import { Config } from '../../config'; -import { type EventPayload, OnEvent } from '../../event'; -import { metrics } from '../../metrics'; -import { PrismaService } from '../../prisma'; +import { + Config, + type EventPayload, + metrics, + OnEvent, + PrismaService, +} from '../../fundamentals'; import { QuotaService } from '../quota'; import { Permission } from '../workspaces/types'; import { isEmptyBuffer } from './manager'; diff --git a/packages/backend/server/src/modules/doc/manager.ts b/packages/backend/server/src/modules/doc/manager.ts index 4ac2424f92..9484fc50cc 100644 --- a/packages/backend/server/src/modules/doc/manager.ts +++ b/packages/backend/server/src/modules/doc/manager.ts @@ -16,12 +16,16 @@ import { transact, } from 'yjs'; -import { Cache } from '../../cache'; -import { Config } from '../../config'; -import { EventEmitter, type EventPayload, OnEvent } from '../../event'; -import { metrics } from '../../metrics/metrics'; -import { PrismaService } from '../../prisma'; -import { mergeUpdatesInApplyWay as jwstMergeUpdates } from '../../storage'; +import { + Cache, + Config, + EventEmitter, + type EventPayload, + mergeUpdatesInApplyWay as jwstMergeUpdates, + metrics, + OnEvent, + PrismaService, +} from '../../fundamentals'; function compare(yBinary: Buffer, jwstBinary: Buffer, strict = false): boolean { if (yBinary.equals(jwstBinary)) { diff --git a/packages/backend/server/src/modules/features/feature.ts b/packages/backend/server/src/modules/features/feature.ts index 3945790b66..99c78a4eba 100644 --- a/packages/backend/server/src/modules/features/feature.ts +++ b/packages/backend/server/src/modules/features/feature.ts @@ -1,4 +1,5 @@ -import { PrismaService } from '../../prisma'; +import { PrismaClient } from '@prisma/client'; + import { Feature, FeatureSchema, FeatureType } from './types'; class FeatureConfig { @@ -66,7 +67,7 @@ export type FeatureConfigType = InstanceType< const FeatureCache = new Map>(); -export async function getFeature(prisma: PrismaService, featureId: number) { +export async function getFeature(prisma: PrismaClient, featureId: number) { const cachedQuota = FeatureCache.get(featureId); if (cachedQuota) { diff --git a/packages/backend/server/src/modules/features/index.ts b/packages/backend/server/src/modules/features/index.ts index ee8d61a283..d29a7dbfe7 100644 --- a/packages/backend/server/src/modules/features/index.ts +++ b/packages/backend/server/src/modules/features/index.ts @@ -1,6 +1,5 @@ import { Module } from '@nestjs/common'; -import { PrismaService } from '../../prisma'; import { FeatureManagementService } from './management'; import { FeatureService } from './service'; @@ -18,4 +17,4 @@ export class FeatureModule {} export { type CommonFeature, commonFeatureSchema } from './types'; export { FeatureKind, Features, FeatureType } from './types'; -export { FeatureManagementService, FeatureService, PrismaService }; +export { FeatureManagementService, FeatureService }; diff --git a/packages/backend/server/src/modules/features/management.ts b/packages/backend/server/src/modules/features/management.ts index 57766fc519..75f11ba1e3 100644 --- a/packages/backend/server/src/modules/features/management.ts +++ b/packages/backend/server/src/modules/features/management.ts @@ -1,7 +1,6 @@ import { Injectable, Logger } from '@nestjs/common'; -import { Config } from '../../config'; -import { PrismaService } from '../../prisma'; +import { Config, PrismaService } from '../../fundamentals'; import { FeatureService } from './service'; import { FeatureType } from './types'; diff --git a/packages/backend/server/src/modules/features/service.ts b/packages/backend/server/src/modules/features/service.ts index b7bf53c023..23b442492e 100644 --- a/packages/backend/server/src/modules/features/service.ts +++ b/packages/backend/server/src/modules/features/service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; -import { PrismaService } from '../../prisma'; +import { PrismaService } from '../../fundamentals'; import { UserType } from '../users/types'; import { WorkspaceType } from '../workspaces/types'; import { FeatureConfigType, getFeature } from './feature'; diff --git a/packages/backend/server/src/modules/index.ts b/packages/backend/server/src/modules/index.ts index 0522f3e2dd..fca3f4e9d6 100644 --- a/packages/backend/server/src/modules/index.ts +++ b/packages/backend/server/src/modules/index.ts @@ -4,8 +4,7 @@ import { DynamicModule, Type } from '@nestjs/common'; import { ScheduleModule } from '@nestjs/schedule'; import { ServeStaticModule } from '@nestjs/serve-static'; -import { SERVER_FLAVOR } from '../config'; -import { GqlModule } from '../graphql.module'; +import { GqlModule } from '../fundamentals/graphql'; import { ServerConfigModule } from './config'; import { DocModule } from './doc'; import { PaymentModule } from './payment'; @@ -18,7 +17,7 @@ import { WorkspaceModule } from './workspaces'; const BusinessModules: (Type | DynamicModule)[] = []; -switch (SERVER_FLAVOR) { +switch (AFFiNE.flavor) { case 'sync': BusinessModules.push(SyncModule, DocModule); break; @@ -68,4 +67,4 @@ switch (SERVER_FLAVOR) { break; } -export { BusinessModules, SERVER_FLAVOR }; +export { BusinessModules }; diff --git a/packages/backend/server/src/modules/payment/resolver.ts b/packages/backend/server/src/modules/payment/resolver.ts index 282d63acaa..be95d24d10 100644 --- a/packages/backend/server/src/modules/payment/resolver.ts +++ b/packages/backend/server/src/modules/payment/resolver.ts @@ -16,8 +16,7 @@ import type { User, UserInvoice, UserSubscription } from '@prisma/client'; import { GraphQLError } from 'graphql'; import { groupBy } from 'lodash-es'; -import { Config } from '../../config'; -import { PrismaService } from '../../prisma'; +import { Config, PrismaService } from '../../fundamentals'; import { Auth, CurrentUser, Public } from '../auth'; import { UserType } from '../users'; import { diff --git a/packages/backend/server/src/modules/payment/service.ts b/packages/backend/server/src/modules/payment/service.ts index e858e12529..137c3585bd 100644 --- a/packages/backend/server/src/modules/payment/service.ts +++ b/packages/backend/server/src/modules/payment/service.ts @@ -9,8 +9,7 @@ import type { } from '@prisma/client'; import Stripe from 'stripe'; -import { Config } from '../../config'; -import { PrismaService } from '../../prisma'; +import { Config, PrismaService } from '../../fundamentals'; import { FeatureManagementService } from '../features'; import { QuotaService, QuotaType } from '../quota'; import { ScheduleManager } from './schedule'; diff --git a/packages/backend/server/src/modules/payment/stripe.ts b/packages/backend/server/src/modules/payment/stripe.ts index 4538471121..4f17d8ded3 100644 --- a/packages/backend/server/src/modules/payment/stripe.ts +++ b/packages/backend/server/src/modules/payment/stripe.ts @@ -2,7 +2,7 @@ import { FactoryProvider } from '@nestjs/common'; import { omit } from 'lodash-es'; import Stripe from 'stripe'; -import { Config } from '../../config'; +import { Config } from '../../fundamentals'; export const StripeProvider: FactoryProvider = { provide: Stripe, diff --git a/packages/backend/server/src/modules/payment/webhook.ts b/packages/backend/server/src/modules/payment/webhook.ts index 13f692132d..fa428e87e0 100644 --- a/packages/backend/server/src/modules/payment/webhook.ts +++ b/packages/backend/server/src/modules/payment/webhook.ts @@ -10,7 +10,7 @@ import { EventEmitter2 } from '@nestjs/event-emitter'; import type { Request } from 'express'; import Stripe from 'stripe'; -import { Config } from '../../config'; +import { Config } from '../../fundamentals'; @Controller('/api/stripe') export class StripeWebhook { diff --git a/packages/backend/server/src/modules/quota/quota.ts b/packages/backend/server/src/modules/quota/quota.ts index caf7c570eb..990eb30479 100644 --- a/packages/backend/server/src/modules/quota/quota.ts +++ b/packages/backend/server/src/modules/quota/quota.ts @@ -1,4 +1,4 @@ -import { PrismaService } from '../../prisma'; +import { PrismaService } from '../../fundamentals'; import { formatDate, formatSize, Quota, QuotaSchema } from './types'; const QuotaCache = new Map(); diff --git a/packages/backend/server/src/modules/quota/service.ts b/packages/backend/server/src/modules/quota/service.ts index cfafbbc3f0..5f05a59e57 100644 --- a/packages/backend/server/src/modules/quota/service.ts +++ b/packages/backend/server/src/modules/quota/service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; -import { PrismaService } from '../../prisma'; +import { PrismaService } from '../../fundamentals'; import { FeatureKind } from '../features'; import { QuotaConfig } from './quota'; import { QuotaType } from './types'; diff --git a/packages/backend/server/src/modules/storage/wrappers/avatar.ts b/packages/backend/server/src/modules/storage/wrappers/avatar.ts index 6622631bd8..c8598b65bf 100644 --- a/packages/backend/server/src/modules/storage/wrappers/avatar.ts +++ b/packages/backend/server/src/modules/storage/wrappers/avatar.ts @@ -1,18 +1,17 @@ import { Injectable } from '@nestjs/common'; -import { AFFiNEStorageConfig, Config } from '../../../config'; -import { type EventPayload, OnEvent } from '../../../event'; -import { +import type { BlobInputType, - createStorageProvider, + EventPayload, PutObjectMetadata, StorageProvider, -} from '../providers'; +} from '../../../fundamentals'; +import { Config, createStorageProvider, OnEvent } from '../../../fundamentals'; @Injectable() export class AvatarStorage { public readonly provider: StorageProvider; - private readonly storageConfig: AFFiNEStorageConfig['storages']['avatar']; + private readonly storageConfig: Config['storage']['storages']['avatar']; constructor(private readonly config: Config) { this.provider = createStorageProvider(this.config.storage, 'avatar'); diff --git a/packages/backend/server/src/modules/storage/wrappers/blob.ts b/packages/backend/server/src/modules/storage/wrappers/blob.ts index 0eca2d4375..80b2b9bc70 100644 --- a/packages/backend/server/src/modules/storage/wrappers/blob.ts +++ b/packages/backend/server/src/modules/storage/wrappers/blob.ts @@ -3,15 +3,19 @@ import { Readable } from 'node:stream'; import type { Storage } from '@affine/storage'; import { Injectable, OnModuleInit } from '@nestjs/common'; -import { Config } from '../../../config'; -import { EventEmitter, type EventPayload, OnEvent } from '../../../event'; -import { OctoBaseStorageModule } from '../../../storage'; -import { +import type { BlobInputType, - createStorageProvider, + EventPayload, StorageProvider, -} from '../providers'; -import { toBuffer } from '../providers/utils'; +} from '../../../fundamentals'; +import { + Config, + createStorageProvider, + EventEmitter, + OctoBaseStorageModule, + OnEvent, + toBuffer, +} from '../../../fundamentals'; @Injectable() export class WorkspaceBlobStorage implements OnModuleInit { diff --git a/packages/backend/server/src/modules/sync/events/events.gateway.ts b/packages/backend/server/src/modules/sync/events/events.gateway.ts index abc1a946d0..5813bf03f8 100644 --- a/packages/backend/server/src/modules/sync/events/events.gateway.ts +++ b/packages/backend/server/src/modules/sync/events/events.gateway.ts @@ -11,12 +11,11 @@ import { import { Server, Socket } from 'socket.io'; import { encodeStateAsUpdate, encodeStateVector } from 'yjs'; -import { metrics } from '../../../metrics'; -import { CallTimer } from '../../../metrics/utils'; -import { DocID } from '../../../utils/doc'; +import { CallTimer, metrics } from '../../../fundamentals'; import { Auth, CurrentUser } from '../../auth'; import { DocManager } from '../../doc'; import { UserType } from '../../users'; +import { DocID } from '../../utils/doc'; import { PermissionService } from '../../workspaces/permission'; import { Permission } from '../../workspaces/types'; import { diff --git a/packages/backend/server/src/modules/sync/utils.ts b/packages/backend/server/src/modules/sync/utils.ts deleted file mode 100644 index ca13e99719..0000000000 --- a/packages/backend/server/src/modules/sync/utils.ts +++ /dev/null @@ -1,11 +0,0 @@ -export function assertExists( - val: T | null | undefined, - message: string | Error = 'val does not exist' -): asserts val is T { - if (val === null || val === undefined) { - if (message instanceof Error) { - throw message; - } - throw new Error(message); - } -} diff --git a/packages/backend/server/src/modules/users/management.ts b/packages/backend/server/src/modules/users/management.ts index a740466b98..08e3bd4def 100644 --- a/packages/backend/server/src/modules/users/management.ts +++ b/packages/backend/server/src/modules/users/management.ts @@ -5,7 +5,7 @@ import { } from '@nestjs/common'; import { Args, Context, Int, Mutation, Query, Resolver } from '@nestjs/graphql'; -import { CloudThrottlerGuard, Throttle } from '../../throttler'; +import { CloudThrottlerGuard, Throttle } from '../../fundamentals'; import { Auth, CurrentUser } from '../auth/guard'; import { AuthService } from '../auth/service'; import { FeatureManagementService } from '../features'; diff --git a/packages/backend/server/src/modules/users/resolver.ts b/packages/backend/server/src/modules/users/resolver.ts index 94cbc2c81d..12842820ae 100644 --- a/packages/backend/server/src/modules/users/resolver.ts +++ b/packages/backend/server/src/modules/users/resolver.ts @@ -11,10 +11,13 @@ import type { User } from '@prisma/client'; import { GraphQLError } from 'graphql'; import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs'; -import { EventEmitter } from '../../event'; -import { PrismaService } from '../../prisma/service'; -import { CloudThrottlerGuard, Throttle } from '../../throttler'; -import type { FileUpload } from '../../types'; +import { + CloudThrottlerGuard, + EventEmitter, + type FileUpload, + PrismaService, + Throttle, +} from '../../fundamentals'; import { Auth, CurrentUser, Public, Publicable } from '../auth/guard'; import { FeatureManagementService } from '../features'; import { QuotaService } from '../quota'; diff --git a/packages/backend/server/src/modules/users/users.ts b/packages/backend/server/src/modules/users/users.ts index 6215ea4433..84a889fd1c 100644 --- a/packages/backend/server/src/modules/users/users.ts +++ b/packages/backend/server/src/modules/users/users.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; -import { PrismaService } from '../../prisma'; +import { PrismaService } from '../../fundamentals'; @Injectable() export class UsersService { diff --git a/packages/backend/server/src/utils/__tests__/doc.spec.ts b/packages/backend/server/src/modules/utils/__tests__/doc.spec.ts similarity index 100% rename from packages/backend/server/src/utils/__tests__/doc.spec.ts rename to packages/backend/server/src/modules/utils/__tests__/doc.spec.ts diff --git a/packages/backend/server/src/utils/doc.ts b/packages/backend/server/src/modules/utils/doc.ts similarity index 100% rename from packages/backend/server/src/utils/doc.ts rename to packages/backend/server/src/modules/utils/doc.ts diff --git a/packages/backend/server/src/modules/workspaces/controller.ts b/packages/backend/server/src/modules/workspaces/controller.ts index e38ed68071..e6528fb7ff 100644 --- a/packages/backend/server/src/modules/workspaces/controller.ts +++ b/packages/backend/server/src/modules/workspaces/controller.ts @@ -9,13 +9,12 @@ import { } from '@nestjs/common'; import type { Response } from 'express'; -import { CallTimer } from '../../metrics'; -import { PrismaService } from '../../prisma'; -import { DocID } from '../../utils/doc'; +import { CallTimer, PrismaService } from '../../fundamentals'; import { Auth, CurrentUser, Publicable } from '../auth'; import { DocHistoryManager, DocManager } from '../doc'; import { WorkspaceBlobStorage } from '../storage'; import { UserType } from '../users'; +import { DocID } from '../utils/doc'; import { PermissionService, PublicPageMode } from './permission'; import { Permission } from './types'; diff --git a/packages/backend/server/src/modules/workspaces/management.ts b/packages/backend/server/src/modules/workspaces/management.ts index b5a74f94ac..635c606499 100644 --- a/packages/backend/server/src/modules/workspaces/management.ts +++ b/packages/backend/server/src/modules/workspaces/management.ts @@ -9,7 +9,7 @@ import { Resolver, } from '@nestjs/graphql'; -import { CloudThrottlerGuard, Throttle } from '../../throttler'; +import { CloudThrottlerGuard, Throttle } from '../../fundamentals'; import { Auth, CurrentUser } from '../auth'; import { FeatureManagementService, FeatureType } from '../features'; import { UserType } from '../users'; diff --git a/packages/backend/server/src/modules/workspaces/permission.ts b/packages/backend/server/src/modules/workspaces/permission.ts index 1248075b1c..2f955131ae 100644 --- a/packages/backend/server/src/modules/workspaces/permission.ts +++ b/packages/backend/server/src/modules/workspaces/permission.ts @@ -1,7 +1,7 @@ import { ForbiddenException, Injectable } from '@nestjs/common'; import { Prisma } from '@prisma/client'; -import { PrismaService } from '../../prisma'; +import { PrismaService } from '../../fundamentals'; import { Permission } from './types'; export enum PublicPageMode { diff --git a/packages/backend/server/src/modules/workspaces/resolvers/blob.ts b/packages/backend/server/src/modules/workspaces/resolvers/blob.ts index ae19169dcb..28d3951752 100644 --- a/packages/backend/server/src/modules/workspaces/resolvers/blob.ts +++ b/packages/backend/server/src/modules/workspaces/resolvers/blob.ts @@ -12,9 +12,12 @@ import { import { GraphQLError } from 'graphql'; import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs'; -import { MakeCache, PreventCache } from '../../../cache'; -import { CloudThrottlerGuard } from '../../../throttler'; -import type { FileUpload } from '../../../types'; +import { + CloudThrottlerGuard, + type FileUpload, + MakeCache, + PreventCache, +} from '../../../fundamentals'; import { Auth, CurrentUser } from '../../auth'; import { FeatureManagementService, FeatureType } from '../../features'; import { QuotaManagementService } from '../../quota'; diff --git a/packages/backend/server/src/modules/workspaces/resolvers/history.ts b/packages/backend/server/src/modules/workspaces/resolvers/history.ts index e7940f9df1..3b8475f3a4 100644 --- a/packages/backend/server/src/modules/workspaces/resolvers/history.ts +++ b/packages/backend/server/src/modules/workspaces/resolvers/history.ts @@ -12,11 +12,11 @@ import { } from '@nestjs/graphql'; import type { SnapshotHistory } from '@prisma/client'; -import { CloudThrottlerGuard } from '../../../throttler'; -import { DocID } from '../../../utils/doc'; +import { CloudThrottlerGuard } from '../../../fundamentals'; import { Auth, CurrentUser } from '../../auth'; -import { DocHistoryManager } from '../../doc/history'; +import { DocHistoryManager } from '../../doc'; import { UserType } from '../../users'; +import { DocID } from '../../utils/doc'; import { PermissionService } from '../permission'; import { Permission, WorkspaceType } from '../types'; diff --git a/packages/backend/server/src/modules/workspaces/resolvers/page.ts b/packages/backend/server/src/modules/workspaces/resolvers/page.ts index c37bc4049c..26454323af 100644 --- a/packages/backend/server/src/modules/workspaces/resolvers/page.ts +++ b/packages/backend/server/src/modules/workspaces/resolvers/page.ts @@ -11,11 +11,10 @@ import { } from '@nestjs/graphql'; import type { WorkspacePage as PrismaWorkspacePage } from '@prisma/client'; -import { PrismaService } from '../../../prisma'; -import { CloudThrottlerGuard } from '../../../throttler'; -import { DocID } from '../../../utils/doc'; +import { CloudThrottlerGuard, PrismaService } from '../../../fundamentals'; import { Auth, CurrentUser } from '../../auth'; import { UserType } from '../../users'; +import { DocID } from '../../utils/doc'; import { PermissionService, PublicPageMode } from '../permission'; import { Permission, WorkspaceType } from '../types'; diff --git a/packages/backend/server/src/modules/workspaces/resolvers/workspace.ts b/packages/backend/server/src/modules/workspaces/resolvers/workspace.ts index 328744e628..ddc7b5dd85 100644 --- a/packages/backend/server/src/modules/workspaces/resolvers/workspace.ts +++ b/packages/backend/server/src/modules/workspaces/resolvers/workspace.ts @@ -20,12 +20,15 @@ import { GraphQLError } from 'graphql'; import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs'; import { applyUpdate, Doc } from 'yjs'; -import { EventEmitter } from '../../../event'; -import { PrismaService } from '../../../prisma'; -import { CloudThrottlerGuard, Throttle } from '../../../throttler'; -import type { FileUpload } from '../../../types'; +import { + CloudThrottlerGuard, + EventEmitter, + type FileUpload, + MailService, + PrismaService, + Throttle, +} from '../../../fundamentals'; import { Auth, CurrentUser, Public } from '../../auth'; -import { MailService } from '../../auth/mailer'; import { AuthService } from '../../auth/service'; import { FeatureManagementService, FeatureType } from '../../features'; import { QuotaManagementService } from '../../quota'; diff --git a/packages/backend/server/src/prelude.ts b/packages/backend/server/src/prelude.ts index d70e339c2c..22a6315fed 100644 --- a/packages/backend/server/src/prelude.ts +++ b/packages/backend/server/src/prelude.ts @@ -1,8 +1,43 @@ import 'reflect-metadata'; -import './affine.env'; -import './affine'; -import './affine.config'; -if (process.env.NODE_ENV === 'development') { - console.log('AFFiNE Config:', globalThis.AFFiNE); +import { join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { config } from 'dotenv'; + +import { + applyEnvToConfig, + getDefaultAFFiNEConfig, +} from './fundamentals/config'; + +async function load() { + // Initializing AFFiNE config + // + // 1. load dotenv file to `process.env` + // load `.env` under pwd + config(); + // load `.env` under user config folder + config({ + path: join(fileURLToPath(import.meta.url), '../config/.env'), + }); + + // 2. generate AFFiNE default config and assign to `globalThis.AFFiNE` + globalThis.AFFiNE = getDefaultAFFiNEConfig(); + + // TODO(@forehalo): + // Modules may contribute to ENV_MAP, figure out a good way to involve them instead of hardcoding in `./config/affine.env` + // 3. load env => config map to `globalThis.AFFiNE.ENV_MAP + await import('./config/affine.env'); + + // 4. apply `process.env` map overriding to `globalThis.AFFiNE` + applyEnvToConfig(globalThis.AFFiNE); + + // 5. load `./config/affine` to patch custom configs + await import('./config/affine'); + + if (process.env.NODE_ENV === 'development') { + console.log('AFFiNE Config:', JSON.stringify(globalThis.AFFiNE, null, 2)); + } } + +await load(); diff --git a/packages/backend/server/src/types.ts b/packages/backend/server/src/types.ts deleted file mode 100644 index 249d2b722b..0000000000 --- a/packages/backend/server/src/types.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Readable } from 'node:stream'; - -export interface FileUpload { - filename: string; - mimetype: string; - encoding: string; - createReadStream: () => Readable; -} - -export interface ReqContext { - req: Express.Request & { - res: Express.Response; - }; -} diff --git a/packages/backend/server/tests/app.e2e.ts b/packages/backend/server/tests/app.e2e.ts index 600737252e..acd38d68e5 100644 --- a/packages/backend/server/tests/app.e2e.ts +++ b/packages/backend/server/tests/app.e2e.ts @@ -4,14 +4,13 @@ import { randomUUID } from 'node:crypto'; import { Transformer } from '@napi-rs/image'; import type { INestApplication } from '@nestjs/common'; import { hashSync } from '@node-rs/argon2'; -import { type User } from '@prisma/client'; +import { PrismaClient, type User } from '@prisma/client'; import ava, { type TestFn } from 'ava'; import type { Express } from 'express'; import request from 'supertest'; import { AppModule } from '../src/app'; import { FeatureManagementService } from '../src/modules/features'; -import { PrismaService } from '../src/prisma/service'; import { createTestingApp } from './utils'; const gql = '/graphql'; @@ -52,7 +51,7 @@ test.beforeEach(async t => { imports: [AppModule], tapModule(builder) { builder - .overrideProvider(PrismaService) + .overrideProvider(PrismaClient) .useClass(FakePrisma) .overrideProvider(FeatureManagementService) .useValue({ canEarlyAccess: () => true }); diff --git a/packages/backend/server/tests/auth.e2e.ts b/packages/backend/server/tests/auth.e2e.ts index 440c189ca6..faa2de65e5 100644 --- a/packages/backend/server/tests/auth.e2e.ts +++ b/packages/backend/server/tests/auth.e2e.ts @@ -5,7 +5,7 @@ import { import type { INestApplication } from '@nestjs/common'; import ava, { type TestFn } from 'ava'; -import { MailService } from '../src/modules/auth/mailer'; +import { MailService } from '../src/fundamentals/mailer'; import { AuthService } from '../src/modules/auth/service'; import { changeEmail, diff --git a/packages/backend/server/tests/auth.spec.ts b/packages/backend/server/tests/auth.spec.ts index 906182ea2a..20d02ed66d 100644 --- a/packages/backend/server/tests/auth.spec.ts +++ b/packages/backend/server/tests/auth.spec.ts @@ -2,10 +2,13 @@ import { TestingModule } from '@nestjs/testing'; import test from 'ava'; -import { ConfigModule } from '../src/config'; +import { ConfigModule } from '../src/fundamentals/config'; +import { + mintChallengeResponse, + verifyChallengeResponse, +} from '../src/fundamentals/storage'; import { AuthResolver } from '../src/modules/auth/resolver'; import { AuthService } from '../src/modules/auth/service'; -import { mintChallengeResponse, verifyChallengeResponse } from '../src/storage'; import { createTestingModule } from './utils'; let authService: AuthService; diff --git a/packages/backend/server/tests/cache.spec.ts b/packages/backend/server/tests/cache.spec.ts index 6ffc2c2f00..ac18126ad9 100644 --- a/packages/backend/server/tests/cache.spec.ts +++ b/packages/backend/server/tests/cache.spec.ts @@ -1,8 +1,8 @@ import { Test, TestingModule } from '@nestjs/testing'; import test from 'ava'; -import { Cache, CacheModule } from '../src/cache'; -import { ConfigModule } from '../src/config'; +import { Cache, CacheModule } from '../src/fundamentals/cache'; +import { ConfigModule } from '../src/fundamentals/config'; let cache: Cache; let module: TestingModule; diff --git a/packages/backend/server/tests/config.spec.ts b/packages/backend/server/tests/config.spec.ts index ad5c19138d..0081d8c8fa 100644 --- a/packages/backend/server/tests/config.spec.ts +++ b/packages/backend/server/tests/config.spec.ts @@ -1,7 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import test from 'ava'; -import { Config, ConfigModule } from '../src/config'; +import { Config, ConfigModule } from '../src/fundamentals/config'; let config: Config; let module: TestingModule; diff --git a/packages/backend/server/tests/doc.spec.ts b/packages/backend/server/tests/doc.spec.ts index d772514c24..08b39223ea 100644 --- a/packages/backend/server/tests/doc.spec.ts +++ b/packages/backend/server/tests/doc.spec.ts @@ -1,6 +1,7 @@ import { mock } from 'node:test'; import { TestingModule } from '@nestjs/testing'; +import { PrismaClient } from '@prisma/client'; import test from 'ava'; import * as Sinon from 'sinon'; import { @@ -10,11 +11,10 @@ import { encodeStateAsUpdate, } from 'yjs'; -import { Config } from '../src/config'; +import { Config } from '../src/fundamentals/config'; import { DocManager, DocModule } from '../src/modules/doc'; import { QuotaModule } from '../src/modules/quota'; import { StorageModule } from '../src/modules/storage'; -import { PrismaService } from '../src/prisma'; import { createTestingModule, initTestingDB } from './utils'; const createModule = () => { @@ -33,7 +33,7 @@ test.beforeEach(async () => { }); m = await createModule(); await m.init(); - await initTestingDB(m.get(PrismaService)); + await initTestingDB(m.get(PrismaClient)); }); test.afterEach.always(async () => { @@ -96,7 +96,7 @@ test('should poll when intervel due', async t => { }); test('should merge update when intervel due', async t => { - const db = m.get(PrismaService); + const db = m.get(PrismaClient); const manager = m.get(DocManager); const doc = new YDoc(); @@ -161,7 +161,7 @@ test('should merge update when intervel due', async t => { }); test('should have sequential update number', async t => { - const db = m.get(PrismaService); + const db = m.get(PrismaClient); const manager = m.get(DocManager); const doc = new YDoc(); const text = doc.getText('content'); @@ -278,7 +278,7 @@ test('should throw if meet max retry times', async t => { }); test('should not update snapshot if state is outdated', async t => { - const db = m.get(PrismaService); + const db = m.get(PrismaClient); const manager = m.get(DocManager); await db.snapshot.create({ diff --git a/packages/backend/server/tests/exception-logger.e2e.ts b/packages/backend/server/tests/exception-logger.e2e.ts deleted file mode 100644 index fccd7117b0..0000000000 --- a/packages/backend/server/tests/exception-logger.e2e.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { Controller, Get, type INestApplication } from '@nestjs/common'; -import { Test } from '@nestjs/testing'; -import test from 'ava'; -import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs'; -import request from 'supertest'; - -import { AppModule } from '../src/app'; -import { ExceptionLogger } from '../src/middleware/exception-logger'; -import { FeatureManagementService } from '../src/modules/features'; -import { PrismaService } from '../src/prisma'; - -const gql = '/graphql'; -const rest = '/rest'; - -let app: INestApplication; - -class FakePrisma { - get workspace() { - return { - async findUnique() { - throw new Error('exception from graphql'); - }, - }; - } -} - -@Controller('rest') -export class MockController { - @Get() - test(): string { - throw new Error('exception from rest api'); - } -} - -test.beforeEach(async () => { - const module = await Test.createTestingModule({ - imports: [AppModule], - controllers: [MockController], - }) - .overrideProvider(PrismaService) - .useClass(FakePrisma) - .overrideProvider(FeatureManagementService) - .useValue({}) - .compile(); - app = module.createNestApplication({ - cors: true, - bodyParser: true, - }); - app.useGlobalFilters(new ExceptionLogger()); - app.use( - graphqlUploadExpress({ - maxFileSize: 10 * 1024 * 1024, - maxFiles: 5, - }) - ); - await app.init(); -}); - -test.afterEach.always(async () => { - await app.close(); -}); - -test('should get response from graphql', async t => { - const id = 'workspace'; - - const response = await request(app.getHttpServer()) - .post(gql) - .send({ - name: 'getPublicWorkspace', - query: ` - query getPublicWorkspace($id: String!) { - publicWorkspace(id: $id) { - id - } - } - `, - variables: { id }, - }); - - t.is(response.status, 200); - t.is(response.body.errors[0].message, 'exception from graphql'); -}); - -test('should get response from rest api', async t => { - const response = await request(app.getHttpServer()).get(rest); - - t.is(response.status, 500); - t.is(response.body.error, 'exception from rest api'); -}); diff --git a/packages/backend/server/tests/feature.spec.ts b/packages/backend/server/tests/feature.spec.ts index 20012f7806..267097615e 100644 --- a/packages/backend/server/tests/feature.spec.ts +++ b/packages/backend/server/tests/feature.spec.ts @@ -1,9 +1,10 @@ /// import { INestApplication, Injectable } from '@nestjs/common'; +import { PrismaClient } from '@prisma/client'; import ava, { type TestFn } from 'ava'; -import { ConfigModule } from '../src/config'; +import { ConfigModule } from '../src/fundamentals/config'; import { AuthService } from '../src/modules/auth/service'; import { FeatureManagementService, @@ -14,12 +15,11 @@ import { import { UserType } from '../src/modules/users/types'; import { WorkspaceResolver } from '../src/modules/workspaces/resolvers'; import { Permission } from '../src/modules/workspaces/types'; -import { PrismaService } from '../src/prisma'; import { createTestingApp } from './utils'; @Injectable() class WorkspaceResolverMock { - constructor(private readonly prisma: PrismaService) {} + constructor(private readonly prisma: PrismaClient) {} async createWorkspace(user: UserType, _init: null) { const workspace = await this.prisma.workspace.create({ diff --git a/packages/backend/server/tests/history.spec.ts b/packages/backend/server/tests/history.spec.ts index 7d416bd08d..da87c4578b 100644 --- a/packages/backend/server/tests/history.spec.ts +++ b/packages/backend/server/tests/history.spec.ts @@ -1,18 +1,18 @@ import { TestingModule } from '@nestjs/testing'; import type { Snapshot } from '@prisma/client'; +import { PrismaClient } from '@prisma/client'; import test from 'ava'; import * as Sinon from 'sinon'; -import { type EventPayload } from '../src/event'; +import { type EventPayload } from '../src/fundamentals/event'; import { DocHistoryManager } from '../src/modules/doc'; import { QuotaModule } from '../src/modules/quota'; import { StorageModule } from '../src/modules/storage'; -import { PrismaService } from '../src/prisma'; import { createTestingModule } from './utils'; let m: TestingModule; let manager: DocHistoryManager; -let db: PrismaService; +let db: PrismaClient; // cleanup database before each test test.beforeEach(async () => { @@ -25,7 +25,7 @@ test.beforeEach(async () => { Sinon.stub(manager, 'getExpiredDateFromNow').resolves( new Date(Date.now() + 1000) ); - db = m.get(PrismaService); + db = m.get(PrismaClient); }); test.afterEach.always(async () => { diff --git a/packages/backend/server/tests/mailer.e2e.ts b/packages/backend/server/tests/mailer.e2e.ts index 0e948dfcc9..21039981b3 100644 --- a/packages/backend/server/tests/mailer.e2e.ts +++ b/packages/backend/server/tests/mailer.e2e.ts @@ -10,7 +10,7 @@ import { TestingModule } from '@nestjs/testing'; import { PrismaClient } from '@prisma/client'; import ava, { type TestFn } from 'ava'; -import { ConfigModule } from '../src/config'; +import { ConfigModule } from '../src/fundamentals/config'; import { AuthService } from '../src/modules/auth/service'; import { createTestingModule } from './utils'; diff --git a/packages/backend/server/tests/mailer.spec.ts b/packages/backend/server/tests/mailer.spec.ts index 7f45664ec6..b01bcfb931 100644 --- a/packages/backend/server/tests/mailer.spec.ts +++ b/packages/backend/server/tests/mailer.spec.ts @@ -3,13 +3,13 @@ import { randomUUID } from 'node:crypto'; import type { INestApplication } from '@nestjs/common'; import { hashSync } from '@node-rs/argon2'; import { type User } from '@prisma/client'; +import { PrismaClient } from '@prisma/client'; import ava, { type TestFn } from 'ava'; import { AppModule } from '../src/app'; -import { MailService } from '../src/modules/auth/mailer'; +import { MailService } from '../src/fundamentals/mailer'; import { FeatureKind, FeatureManagementService } from '../src/modules/features'; import { Quotas } from '../src/modules/quota'; -import { PrismaService } from '../src/prisma'; import { createTestingApp, createWorkspace, @@ -131,7 +131,7 @@ test.beforeEach(async t => { imports: [AppModule], tapModule: module => { module - .overrideProvider(PrismaService) + .overrideProvider(PrismaClient) .useValue(FakePrisma) .overrideProvider(FeatureManagementService) .useValue({ diff --git a/packages/backend/server/tests/quota.spec.ts b/packages/backend/server/tests/quota.spec.ts index 75c7662a04..99d7fe5d29 100644 --- a/packages/backend/server/tests/quota.spec.ts +++ b/packages/backend/server/tests/quota.spec.ts @@ -3,7 +3,7 @@ import { TestingModule } from '@nestjs/testing'; import ava, { type TestFn } from 'ava'; -import { AuthService } from '../src/modules/auth/service'; +import { AuthService } from '../src/modules/auth'; import { QuotaManagementService, QuotaModule, diff --git a/packages/backend/server/tests/session.spec.ts b/packages/backend/server/tests/session.spec.ts index 8814011d39..6c5852a82d 100644 --- a/packages/backend/server/tests/session.spec.ts +++ b/packages/backend/server/tests/session.spec.ts @@ -3,9 +3,9 @@ import { TestingModule } from '@nestjs/testing'; import ava, { type TestFn } from 'ava'; -import { CacheModule } from '../src/cache'; -import { ConfigModule } from '../src/config'; -import { SessionModule, SessionService } from '../src/session'; +import { CacheModule } from '../src/fundamentals/cache'; +import { ConfigModule } from '../src/fundamentals/config'; +import { SessionModule, SessionService } from '../src/fundamentals/session'; import { createTestingModule } from './utils'; const test = ava as TestFn<{ diff --git a/packages/backend/server/tests/utils/utils.ts b/packages/backend/server/tests/utils/utils.ts index 334a78941e..fcfd25c3b6 100644 --- a/packages/backend/server/tests/utils/utils.ts +++ b/packages/backend/server/tests/utils/utils.ts @@ -6,7 +6,7 @@ import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs'; import { AppModule, FunctionalityModules } from '../../src/app'; import { UserFeaturesInit1698652531198 } from '../../src/data/migrations/1698652531198-user-features-init'; -import { GqlModule } from '../../src/graphql.module'; +import { GqlModule } from '../../src/fundamentals/graphql'; async function flushDB(client: PrismaClient) { const result: { tablename: string }[] = diff --git a/packages/backend/server/tests/workspace-invite.e2e.ts b/packages/backend/server/tests/workspace-invite.e2e.ts index 8ecc747290..60e43175b0 100644 --- a/packages/backend/server/tests/workspace-invite.e2e.ts +++ b/packages/backend/server/tests/workspace-invite.e2e.ts @@ -7,9 +7,8 @@ import { PrismaClient } from '@prisma/client'; import ava, { type TestFn } from 'ava'; import { AppModule } from '../src/app'; -import { MailService } from '../src/modules/auth/mailer'; +import { MailService } from '../src/fundamentals/mailer'; import { AuthService } from '../src/modules/auth/service'; -import { PrismaService } from '../src/prisma'; import { acceptInviteById, createTestingApp, @@ -33,7 +32,7 @@ test.beforeEach(async t => { imports: [AppModule], }); t.context.app = app; - t.context.client = app.get(PrismaService); + t.context.client = app.get(PrismaClient); t.context.auth = app.get(AuthService); t.context.mail = app.get(MailService); }); diff --git a/packages/backend/server/tests/workspace.e2e.ts b/packages/backend/server/tests/workspace.e2e.ts index 8483a92141..4ea4e592eb 100644 --- a/packages/backend/server/tests/workspace.e2e.ts +++ b/packages/backend/server/tests/workspace.e2e.ts @@ -4,7 +4,6 @@ import ava, { type TestFn } from 'ava'; import request from 'supertest'; import { AppModule } from '../src/app'; -import { PrismaService } from '../src/prisma'; import { acceptInviteById, createTestingApp, @@ -29,7 +28,7 @@ test.beforeEach(async t => { imports: [AppModule], }); - t.context.client = app.get(PrismaService); + t.context.client = app.get(PrismaClient); t.context.app = app; }); From 3602f1cac0fd3d6d11ec7a812cb7d1c42116fcff Mon Sep 17 00:00:00 2001 From: regischen Date: Fri, 12 Jan 2024 12:41:27 +0800 Subject: [PATCH 11/92] feat: bump blocksuite (#5575) --- packages/common/env/package.json | 4 +- packages/common/infra/package.json | 10 +- packages/common/y-indexeddb/package.json | 6 +- packages/common/y-provider/package.json | 2 +- packages/frontend/component/package.json | 10 +- packages/frontend/core/package.json | 14 +- .../get-content-parser.ts | 18 -- .../core/src/hooks/affine/use-export-page.ts | 33 +-- packages/frontend/electron/package.json | 8 +- tests/affine-legacy/0.6.1-beta.1/package.json | 8 +- .../0.7.0-canary.18/package.json | 8 +- .../affine-legacy/0.8.0-canary.7/package.json | 8 +- tests/affine-legacy/0.8.4/package.json | 8 +- tests/affine-migration/package.json | 8 +- tests/storybook/package.json | 14 +- yarn.lock | 277 ++++++++++-------- 16 files changed, 221 insertions(+), 215 deletions(-) delete mode 100644 packages/frontend/core/src/components/page-list/operation-menu-items/get-content-parser.ts diff --git a/packages/common/env/package.json b/packages/common/env/package.json index 10ce0c5c7d..f202db8226 100644 --- a/packages/common/env/package.json +++ b/packages/common/env/package.json @@ -3,8 +3,8 @@ "private": true, "type": "module", "devDependencies": { - "@blocksuite/global": "0.11.0-nightly-202401020419-752a5b8", - "@blocksuite/store": "0.11.0-nightly-202401020419-752a5b8", + "@blocksuite/global": "0.12.0-nightly-202401120404-4219e86", + "@blocksuite/store": "0.12.0-nightly-202401120404-4219e86", "react": "18.2.0", "react-dom": "18.2.0", "vitest": "1.1.3" diff --git a/packages/common/infra/package.json b/packages/common/infra/package.json index a3dfb401ba..5222c21045 100644 --- a/packages/common/infra/package.json +++ b/packages/common/infra/package.json @@ -13,9 +13,9 @@ "@affine/debug": "workspace:*", "@affine/env": "workspace:*", "@affine/templates": "workspace:*", - "@blocksuite/blocks": "0.11.0-nightly-202401020419-752a5b8", - "@blocksuite/global": "0.11.0-nightly-202401020419-752a5b8", - "@blocksuite/store": "0.11.0-nightly-202401020419-752a5b8", + "@blocksuite/blocks": "0.12.0-nightly-202401120404-4219e86", + "@blocksuite/global": "0.12.0-nightly-202401120404-4219e86", + "@blocksuite/store": "0.12.0-nightly-202401120404-4219e86", "jotai": "^2.5.1", "jotai-effect": "^0.2.3", "nanoid": "^5.0.3", @@ -26,8 +26,8 @@ "devDependencies": { "@affine-test/fixtures": "workspace:*", "@affine/templates": "workspace:*", - "@blocksuite/lit": "0.11.0-nightly-202401020419-752a5b8", - "@blocksuite/presets": "0.11.0-nightly-202401020419-752a5b8", + "@blocksuite/lit": "0.12.0-nightly-202401120404-4219e86", + "@blocksuite/presets": "0.12.0-nightly-202401120404-4219e86", "async-call-rpc": "^6.3.1", "react": "^18.2.0", "rxjs": "^7.8.1", diff --git a/packages/common/y-indexeddb/package.json b/packages/common/y-indexeddb/package.json index 9a2ec7dc7a..57ddc13099 100644 --- a/packages/common/y-indexeddb/package.json +++ b/packages/common/y-indexeddb/package.json @@ -32,14 +32,14 @@ } }, "dependencies": { - "@blocksuite/global": "0.11.0-nightly-202401020419-752a5b8", + "@blocksuite/global": "0.12.0-nightly-202401120404-4219e86", "idb": "^8.0.0", "nanoid": "^5.0.3", "y-provider": "workspace:*" }, "devDependencies": { - "@blocksuite/blocks": "0.11.0-nightly-202401020419-752a5b8", - "@blocksuite/store": "0.11.0-nightly-202401020419-752a5b8", + "@blocksuite/blocks": "0.12.0-nightly-202401120404-4219e86", + "@blocksuite/store": "0.12.0-nightly-202401120404-4219e86", "fake-indexeddb": "^5.0.0", "vite": "^5.0.6", "vite-plugin-dts": "3.7.0", diff --git a/packages/common/y-provider/package.json b/packages/common/y-provider/package.json index cb815e38d8..fc7af90e84 100644 --- a/packages/common/y-provider/package.json +++ b/packages/common/y-provider/package.json @@ -24,7 +24,7 @@ "build": "vite build" }, "devDependencies": { - "@blocksuite/store": "0.11.0-nightly-202401020419-752a5b8", + "@blocksuite/store": "0.12.0-nightly-202401120404-4219e86", "vite": "^5.0.6", "vite-plugin-dts": "3.7.0", "vitest": "1.1.3", diff --git a/packages/frontend/component/package.json b/packages/frontend/component/package.json index 2df0a65915..bfcc434638 100644 --- a/packages/frontend/component/package.json +++ b/packages/frontend/component/package.json @@ -71,12 +71,12 @@ "uuid": "^9.0.1" }, "devDependencies": { - "@blocksuite/blocks": "0.11.0-nightly-202401020419-752a5b8", - "@blocksuite/global": "0.11.0-nightly-202401020419-752a5b8", + "@blocksuite/blocks": "0.12.0-nightly-202401120404-4219e86", + "@blocksuite/global": "0.12.0-nightly-202401120404-4219e86", "@blocksuite/icons": "2.1.40", - "@blocksuite/lit": "0.11.0-nightly-202401020419-752a5b8", - "@blocksuite/presets": "0.11.0-nightly-202401020419-752a5b8", - "@blocksuite/store": "0.11.0-nightly-202401020419-752a5b8", + "@blocksuite/lit": "0.12.0-nightly-202401120404-4219e86", + "@blocksuite/presets": "0.12.0-nightly-202401120404-4219e86", + "@blocksuite/store": "0.12.0-nightly-202401120404-4219e86", "@storybook/addon-actions": "^7.5.3", "@storybook/addon-essentials": "^7.5.3", "@storybook/addon-interactions": "^7.5.3", diff --git a/packages/frontend/core/package.json b/packages/frontend/core/package.json index 5f9e800f70..d4f8bdab23 100644 --- a/packages/frontend/core/package.json +++ b/packages/frontend/core/package.json @@ -26,14 +26,14 @@ "@affine/templates": "workspace:*", "@affine/workspace": "workspace:*", "@affine/workspace-impl": "workspace:*", - "@blocksuite/block-std": "0.11.0-nightly-202401020419-752a5b8", - "@blocksuite/blocks": "0.11.0-nightly-202401020419-752a5b8", - "@blocksuite/global": "0.11.0-nightly-202401020419-752a5b8", + "@blocksuite/block-std": "0.12.0-nightly-202401120404-4219e86", + "@blocksuite/blocks": "0.12.0-nightly-202401120404-4219e86", + "@blocksuite/global": "0.12.0-nightly-202401120404-4219e86", "@blocksuite/icons": "2.1.40", - "@blocksuite/inline": "0.11.0-nightly-202401020419-752a5b8", - "@blocksuite/lit": "0.11.0-nightly-202401020419-752a5b8", - "@blocksuite/presets": "0.11.0-nightly-202401020419-752a5b8", - "@blocksuite/store": "0.11.0-nightly-202401020419-752a5b8", + "@blocksuite/inline": "0.12.0-nightly-202401120404-4219e86", + "@blocksuite/lit": "0.12.0-nightly-202401120404-4219e86", + "@blocksuite/presets": "0.12.0-nightly-202401120404-4219e86", + "@blocksuite/store": "0.12.0-nightly-202401120404-4219e86", "@dnd-kit/core": "^6.0.8", "@dnd-kit/sortable": "^8.0.0", "@emotion/cache": "^11.11.0", diff --git a/packages/frontend/core/src/components/page-list/operation-menu-items/get-content-parser.ts b/packages/frontend/core/src/components/page-list/operation-menu-items/get-content-parser.ts deleted file mode 100644 index 17106025ff..0000000000 --- a/packages/frontend/core/src/components/page-list/operation-menu-items/get-content-parser.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ContentParser } from '@blocksuite/blocks/content-parser'; -import type { Page } from '@blocksuite/store'; - -const contentParserWeakMap = new WeakMap(); - -export function getContentParser(page: Page) { - if (!contentParserWeakMap.has(page)) { - contentParserWeakMap.set( - page, - new ContentParser(page, { - imageProxyEndpoint: !environment.isDesktop - ? runtimeConfig.imageProxyUrl - : undefined, - }) - ); - } - return contentParserWeakMap.get(page) as ContentParser; -} diff --git a/packages/frontend/core/src/hooks/affine/use-export-page.ts b/packages/frontend/core/src/hooks/affine/use-export-page.ts index 7ad1714c79..56a5b1c9b1 100644 --- a/packages/frontend/core/src/hooks/affine/use-export-page.ts +++ b/packages/frontend/core/src/hooks/affine/use-export-page.ts @@ -5,12 +5,12 @@ import { import { pushNotificationAtom } from '@affine/component/notification-center'; import { apis } from '@affine/electron-api'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; +import type { PageService } from '@blocksuite/blocks'; import { HtmlTransformer, MarkdownTransformer, type PageBlockModel, } from '@blocksuite/blocks'; -import { ContentParser } from '@blocksuite/blocks/content-parser'; import type { Page } from '@blocksuite/store'; import { useSetAtom } from 'jotai'; import { nanoid } from 'nanoid'; @@ -18,29 +18,17 @@ import { useCallback } from 'react'; type ExportType = 'pdf' | 'html' | 'png' | 'markdown'; -const contentParserWeakMap = new WeakMap(); - -const getContentParser = (page: Page) => { - if (!contentParserWeakMap.has(page)) { - contentParserWeakMap.set( - page, - new ContentParser(page, { - imageProxyEndpoint: !environment.isDesktop - ? runtimeConfig.imageProxyUrl - : undefined, - }) - ); - } - return contentParserWeakMap.get(page) as ContentParser; -}; - interface ExportHandlerOptions { page: Page; type: ExportType; } async function exportHandler({ page, type }: ExportHandlerOptions) { - const contentParser = getContentParser(page); + const editorRoot = document.querySelector('editor-host'); + let pageService: PageService | null = null; + if (editorRoot) { + pageService = editorRoot.spec.getService('affine:page') as PageService; + } switch (type) { case 'html': await HtmlTransformer.exportPage(page); @@ -54,12 +42,15 @@ async function exportHandler({ page, type }: ExportHandlerOptions) { (page.root as PageBlockModel).title.toString() ); } else { - await contentParser['exportPdf'](); + if (!pageService) return; + await pageService.exportManager.exportPdf(); } break; - case 'png': - await contentParser['exportPng'](); + case 'png': { + if (!pageService) return; + await pageService.exportManager.exportPng(); break; + } } } diff --git a/packages/frontend/electron/package.json b/packages/frontend/electron/package.json index fa70114ea2..76bc9a61d4 100644 --- a/packages/frontend/electron/package.json +++ b/packages/frontend/electron/package.json @@ -25,10 +25,10 @@ "@affine-test/kit": "workspace:*", "@affine/env": "workspace:*", "@affine/native": "workspace:*", - "@blocksuite/blocks": "0.11.0-nightly-202401020419-752a5b8", - "@blocksuite/lit": "0.11.0-nightly-202401020419-752a5b8", - "@blocksuite/presets": "0.11.0-nightly-202401020419-752a5b8", - "@blocksuite/store": "0.11.0-nightly-202401020419-752a5b8", + "@blocksuite/blocks": "0.12.0-nightly-202401120404-4219e86", + "@blocksuite/lit": "0.12.0-nightly-202401120404-4219e86", + "@blocksuite/presets": "0.12.0-nightly-202401120404-4219e86", + "@blocksuite/store": "0.12.0-nightly-202401120404-4219e86", "@electron-forge/cli": "^7.2.0", "@electron-forge/core": "^7.2.0", "@electron-forge/core-utils": "^7.2.0", diff --git a/tests/affine-legacy/0.6.1-beta.1/package.json b/tests/affine-legacy/0.6.1-beta.1/package.json index fa7600577e..2a3f80f43f 100644 --- a/tests/affine-legacy/0.6.1-beta.1/package.json +++ b/tests/affine-legacy/0.6.1-beta.1/package.json @@ -9,10 +9,10 @@ "devDependencies": { "@affine-test/fixtures": "workspace:*", "@affine-test/kit": "workspace:*", - "@blocksuite/block-std": "0.11.0-nightly-202401020419-752a5b8", - "@blocksuite/blocks": "0.11.0-nightly-202401020419-752a5b8", - "@blocksuite/global": "0.11.0-nightly-202401020419-752a5b8", - "@blocksuite/store": "0.11.0-nightly-202401020419-752a5b8", + "@blocksuite/block-std": "0.12.0-nightly-202401120404-4219e86", + "@blocksuite/blocks": "0.12.0-nightly-202401120404-4219e86", + "@blocksuite/global": "0.12.0-nightly-202401120404-4219e86", + "@blocksuite/store": "0.12.0-nightly-202401120404-4219e86", "@playwright/test": "^1.39.0", "express": "^4.18.2", "http-proxy-middleware": "^3.0.0-beta.1", diff --git a/tests/affine-legacy/0.7.0-canary.18/package.json b/tests/affine-legacy/0.7.0-canary.18/package.json index e1c3213594..7a6e0d72de 100644 --- a/tests/affine-legacy/0.7.0-canary.18/package.json +++ b/tests/affine-legacy/0.7.0-canary.18/package.json @@ -9,10 +9,10 @@ "devDependencies": { "@affine-test/fixtures": "workspace:*", "@affine-test/kit": "workspace:*", - "@blocksuite/block-std": "0.11.0-nightly-202401020419-752a5b8", - "@blocksuite/blocks": "0.11.0-nightly-202401020419-752a5b8", - "@blocksuite/global": "0.11.0-nightly-202401020419-752a5b8", - "@blocksuite/store": "0.11.0-nightly-202401020419-752a5b8", + "@blocksuite/block-std": "0.12.0-nightly-202401120404-4219e86", + "@blocksuite/blocks": "0.12.0-nightly-202401120404-4219e86", + "@blocksuite/global": "0.12.0-nightly-202401120404-4219e86", + "@blocksuite/store": "0.12.0-nightly-202401120404-4219e86", "@playwright/test": "^1.39.0", "express": "^4.18.2", "http-proxy-middleware": "^3.0.0-beta.1", diff --git a/tests/affine-legacy/0.8.0-canary.7/package.json b/tests/affine-legacy/0.8.0-canary.7/package.json index 4271ae7c10..ceab4e5a31 100644 --- a/tests/affine-legacy/0.8.0-canary.7/package.json +++ b/tests/affine-legacy/0.8.0-canary.7/package.json @@ -9,10 +9,10 @@ "devDependencies": { "@affine-test/fixtures": "workspace:*", "@affine-test/kit": "workspace:*", - "@blocksuite/block-std": "0.11.0-nightly-202401020419-752a5b8", - "@blocksuite/blocks": "0.11.0-nightly-202401020419-752a5b8", - "@blocksuite/global": "0.11.0-nightly-202401020419-752a5b8", - "@blocksuite/store": "0.11.0-nightly-202401020419-752a5b8", + "@blocksuite/block-std": "0.12.0-nightly-202401120404-4219e86", + "@blocksuite/blocks": "0.12.0-nightly-202401120404-4219e86", + "@blocksuite/global": "0.12.0-nightly-202401120404-4219e86", + "@blocksuite/store": "0.12.0-nightly-202401120404-4219e86", "@playwright/test": "^1.39.0", "express": "^4.18.2", "http-proxy-middleware": "^3.0.0-beta.1", diff --git a/tests/affine-legacy/0.8.4/package.json b/tests/affine-legacy/0.8.4/package.json index 1c33f58610..e36c6ce7b1 100644 --- a/tests/affine-legacy/0.8.4/package.json +++ b/tests/affine-legacy/0.8.4/package.json @@ -9,10 +9,10 @@ "devDependencies": { "@affine-test/fixtures": "workspace:*", "@affine-test/kit": "workspace:*", - "@blocksuite/block-std": "0.11.0-nightly-202401020419-752a5b8", - "@blocksuite/blocks": "0.11.0-nightly-202401020419-752a5b8", - "@blocksuite/global": "0.11.0-nightly-202401020419-752a5b8", - "@blocksuite/store": "0.11.0-nightly-202401020419-752a5b8", + "@blocksuite/block-std": "0.12.0-nightly-202401120404-4219e86", + "@blocksuite/blocks": "0.12.0-nightly-202401120404-4219e86", + "@blocksuite/global": "0.12.0-nightly-202401120404-4219e86", + "@blocksuite/store": "0.12.0-nightly-202401120404-4219e86", "@playwright/test": "^1.39.0", "express": "^4.18.2", "http-proxy-middleware": "^3.0.0-beta.1", diff --git a/tests/affine-migration/package.json b/tests/affine-migration/package.json index 63fb0892d4..e30d0b312f 100644 --- a/tests/affine-migration/package.json +++ b/tests/affine-migration/package.json @@ -7,10 +7,10 @@ "devDependencies": { "@affine-test/fixtures": "workspace:*", "@affine-test/kit": "workspace:*", - "@blocksuite/block-std": "0.11.0-nightly-202401020419-752a5b8", - "@blocksuite/blocks": "0.11.0-nightly-202401020419-752a5b8", - "@blocksuite/global": "0.11.0-nightly-202401020419-752a5b8", - "@blocksuite/store": "0.11.0-nightly-202401020419-752a5b8", + "@blocksuite/block-std": "0.12.0-nightly-202401120404-4219e86", + "@blocksuite/blocks": "0.12.0-nightly-202401120404-4219e86", + "@blocksuite/global": "0.12.0-nightly-202401120404-4219e86", + "@blocksuite/store": "0.12.0-nightly-202401120404-4219e86", "@playwright/test": "^1.39.0" }, "version": "0.11.0" diff --git a/tests/storybook/package.json b/tests/storybook/package.json index 51822700c2..7819d67df9 100644 --- a/tests/storybook/package.json +++ b/tests/storybook/package.json @@ -21,14 +21,14 @@ "storybook-addon-react-router-v6": "^2.0.10" }, "devDependencies": { - "@blocksuite/block-std": "0.11.0-nightly-202401020419-752a5b8", - "@blocksuite/blocks": "0.11.0-nightly-202401020419-752a5b8", - "@blocksuite/global": "0.11.0-nightly-202401020419-752a5b8", + "@blocksuite/block-std": "0.12.0-nightly-202401120404-4219e86", + "@blocksuite/blocks": "0.12.0-nightly-202401120404-4219e86", + "@blocksuite/global": "0.12.0-nightly-202401120404-4219e86", "@blocksuite/icons": "2.1.40", - "@blocksuite/inline": "0.11.0-nightly-202401020419-752a5b8", - "@blocksuite/lit": "0.11.0-nightly-202401020419-752a5b8", - "@blocksuite/presets": "0.11.0-nightly-202401020419-752a5b8", - "@blocksuite/store": "0.11.0-nightly-202401020419-752a5b8", + "@blocksuite/inline": "0.12.0-nightly-202401120404-4219e86", + "@blocksuite/lit": "0.12.0-nightly-202401120404-4219e86", + "@blocksuite/presets": "0.12.0-nightly-202401120404-4219e86", + "@blocksuite/store": "0.12.0-nightly-202401120404-4219e86", "@storybook/addon-actions": "^7.5.3", "@storybook/addon-essentials": "^7.5.3", "@storybook/addon-interactions": "^7.5.3", diff --git a/yarn.lock b/yarn.lock index 7902485314..ebdd4845df 100644 --- a/yarn.lock +++ b/yarn.lock @@ -25,10 +25,10 @@ __metadata: dependencies: "@affine-test/fixtures": "workspace:*" "@affine-test/kit": "workspace:*" - "@blocksuite/block-std": "npm:0.11.0-nightly-202401020419-752a5b8" - "@blocksuite/blocks": "npm:0.11.0-nightly-202401020419-752a5b8" - "@blocksuite/global": "npm:0.11.0-nightly-202401020419-752a5b8" - "@blocksuite/store": "npm:0.11.0-nightly-202401020419-752a5b8" + "@blocksuite/block-std": "npm:0.12.0-nightly-202401120404-4219e86" + "@blocksuite/blocks": "npm:0.12.0-nightly-202401120404-4219e86" + "@blocksuite/global": "npm:0.12.0-nightly-202401120404-4219e86" + "@blocksuite/store": "npm:0.12.0-nightly-202401120404-4219e86" "@playwright/test": "npm:^1.39.0" express: "npm:^4.18.2" http-proxy-middleware: "npm:^3.0.0-beta.1" @@ -42,10 +42,10 @@ __metadata: dependencies: "@affine-test/fixtures": "workspace:*" "@affine-test/kit": "workspace:*" - "@blocksuite/block-std": "npm:0.11.0-nightly-202401020419-752a5b8" - "@blocksuite/blocks": "npm:0.11.0-nightly-202401020419-752a5b8" - "@blocksuite/global": "npm:0.11.0-nightly-202401020419-752a5b8" - "@blocksuite/store": "npm:0.11.0-nightly-202401020419-752a5b8" + "@blocksuite/block-std": "npm:0.12.0-nightly-202401120404-4219e86" + "@blocksuite/blocks": "npm:0.12.0-nightly-202401120404-4219e86" + "@blocksuite/global": "npm:0.12.0-nightly-202401120404-4219e86" + "@blocksuite/store": "npm:0.12.0-nightly-202401120404-4219e86" "@playwright/test": "npm:^1.39.0" express: "npm:^4.18.2" http-proxy-middleware: "npm:^3.0.0-beta.1" @@ -59,10 +59,10 @@ __metadata: dependencies: "@affine-test/fixtures": "workspace:*" "@affine-test/kit": "workspace:*" - "@blocksuite/block-std": "npm:0.11.0-nightly-202401020419-752a5b8" - "@blocksuite/blocks": "npm:0.11.0-nightly-202401020419-752a5b8" - "@blocksuite/global": "npm:0.11.0-nightly-202401020419-752a5b8" - "@blocksuite/store": "npm:0.11.0-nightly-202401020419-752a5b8" + "@blocksuite/block-std": "npm:0.12.0-nightly-202401120404-4219e86" + "@blocksuite/blocks": "npm:0.12.0-nightly-202401120404-4219e86" + "@blocksuite/global": "npm:0.12.0-nightly-202401120404-4219e86" + "@blocksuite/store": "npm:0.12.0-nightly-202401120404-4219e86" "@playwright/test": "npm:^1.39.0" express: "npm:^4.18.2" http-proxy-middleware: "npm:^3.0.0-beta.1" @@ -76,10 +76,10 @@ __metadata: dependencies: "@affine-test/fixtures": "workspace:*" "@affine-test/kit": "workspace:*" - "@blocksuite/block-std": "npm:0.11.0-nightly-202401020419-752a5b8" - "@blocksuite/blocks": "npm:0.11.0-nightly-202401020419-752a5b8" - "@blocksuite/global": "npm:0.11.0-nightly-202401020419-752a5b8" - "@blocksuite/store": "npm:0.11.0-nightly-202401020419-752a5b8" + "@blocksuite/block-std": "npm:0.12.0-nightly-202401120404-4219e86" + "@blocksuite/blocks": "npm:0.12.0-nightly-202401120404-4219e86" + "@blocksuite/global": "npm:0.12.0-nightly-202401120404-4219e86" + "@blocksuite/store": "npm:0.12.0-nightly-202401120404-4219e86" "@playwright/test": "npm:^1.39.0" express: "npm:^4.18.2" http-proxy-middleware: "npm:^3.0.0-beta.1" @@ -139,10 +139,10 @@ __metadata: dependencies: "@affine-test/fixtures": "workspace:*" "@affine-test/kit": "workspace:*" - "@blocksuite/block-std": "npm:0.11.0-nightly-202401020419-752a5b8" - "@blocksuite/blocks": "npm:0.11.0-nightly-202401020419-752a5b8" - "@blocksuite/global": "npm:0.11.0-nightly-202401020419-752a5b8" - "@blocksuite/store": "npm:0.11.0-nightly-202401020419-752a5b8" + "@blocksuite/block-std": "npm:0.12.0-nightly-202401120404-4219e86" + "@blocksuite/blocks": "npm:0.12.0-nightly-202401120404-4219e86" + "@blocksuite/global": "npm:0.12.0-nightly-202401120404-4219e86" + "@blocksuite/store": "npm:0.12.0-nightly-202401120404-4219e86" "@playwright/test": "npm:^1.39.0" languageName: unknown linkType: soft @@ -206,12 +206,12 @@ __metadata: "@affine/graphql": "workspace:*" "@affine/i18n": "workspace:*" "@affine/workspace": "workspace:*" - "@blocksuite/blocks": "npm:0.11.0-nightly-202401020419-752a5b8" - "@blocksuite/global": "npm:0.11.0-nightly-202401020419-752a5b8" + "@blocksuite/blocks": "npm:0.12.0-nightly-202401120404-4219e86" + "@blocksuite/global": "npm:0.12.0-nightly-202401120404-4219e86" "@blocksuite/icons": "npm:2.1.40" - "@blocksuite/lit": "npm:0.11.0-nightly-202401020419-752a5b8" - "@blocksuite/presets": "npm:0.11.0-nightly-202401020419-752a5b8" - "@blocksuite/store": "npm:0.11.0-nightly-202401020419-752a5b8" + "@blocksuite/lit": "npm:0.12.0-nightly-202401120404-4219e86" + "@blocksuite/presets": "npm:0.12.0-nightly-202401120404-4219e86" + "@blocksuite/store": "npm:0.12.0-nightly-202401120404-4219e86" "@dnd-kit/core": "npm:^6.0.8" "@dnd-kit/modifiers": "npm:^7.0.0" "@dnd-kit/sortable": "npm:^8.0.0" @@ -308,14 +308,14 @@ __metadata: "@affine/workspace": "workspace:*" "@affine/workspace-impl": "workspace:*" "@aws-sdk/client-s3": "npm:3.485.0" - "@blocksuite/block-std": "npm:0.11.0-nightly-202401020419-752a5b8" - "@blocksuite/blocks": "npm:0.11.0-nightly-202401020419-752a5b8" - "@blocksuite/global": "npm:0.11.0-nightly-202401020419-752a5b8" + "@blocksuite/block-std": "npm:0.12.0-nightly-202401120404-4219e86" + "@blocksuite/blocks": "npm:0.12.0-nightly-202401120404-4219e86" + "@blocksuite/global": "npm:0.12.0-nightly-202401120404-4219e86" "@blocksuite/icons": "npm:2.1.40" - "@blocksuite/inline": "npm:0.11.0-nightly-202401020419-752a5b8" - "@blocksuite/lit": "npm:0.11.0-nightly-202401020419-752a5b8" - "@blocksuite/presets": "npm:0.11.0-nightly-202401020419-752a5b8" - "@blocksuite/store": "npm:0.11.0-nightly-202401020419-752a5b8" + "@blocksuite/inline": "npm:0.12.0-nightly-202401120404-4219e86" + "@blocksuite/lit": "npm:0.12.0-nightly-202401120404-4219e86" + "@blocksuite/presets": "npm:0.12.0-nightly-202401120404-4219e86" + "@blocksuite/store": "npm:0.12.0-nightly-202401120404-4219e86" "@dnd-kit/core": "npm:^6.0.8" "@dnd-kit/sortable": "npm:^8.0.0" "@emotion/cache": "npm:^11.11.0" @@ -442,10 +442,10 @@ __metadata: "@affine-test/kit": "workspace:*" "@affine/env": "workspace:*" "@affine/native": "workspace:*" - "@blocksuite/blocks": "npm:0.11.0-nightly-202401020419-752a5b8" - "@blocksuite/lit": "npm:0.11.0-nightly-202401020419-752a5b8" - "@blocksuite/presets": "npm:0.11.0-nightly-202401020419-752a5b8" - "@blocksuite/store": "npm:0.11.0-nightly-202401020419-752a5b8" + "@blocksuite/blocks": "npm:0.12.0-nightly-202401120404-4219e86" + "@blocksuite/lit": "npm:0.12.0-nightly-202401120404-4219e86" + "@blocksuite/presets": "npm:0.12.0-nightly-202401120404-4219e86" + "@blocksuite/store": "npm:0.12.0-nightly-202401120404-4219e86" "@electron-forge/cli": "npm:^7.2.0" "@electron-forge/core": "npm:^7.2.0" "@electron-forge/core-utils": "npm:^7.2.0" @@ -494,8 +494,8 @@ __metadata: version: 0.0.0-use.local resolution: "@affine/env@workspace:packages/common/env" dependencies: - "@blocksuite/global": "npm:0.11.0-nightly-202401020419-752a5b8" - "@blocksuite/store": "npm:0.11.0-nightly-202401020419-752a5b8" + "@blocksuite/global": "npm:0.12.0-nightly-202401120404-4219e86" + "@blocksuite/store": "npm:0.12.0-nightly-202401120404-4219e86" lit: "npm:^3.0.2" react: "npm:18.2.0" react-dom: "npm:18.2.0" @@ -740,14 +740,14 @@ __metadata: dependencies: "@affine/component": "workspace:*" "@affine/i18n": "workspace:*" - "@blocksuite/block-std": "npm:0.11.0-nightly-202401020419-752a5b8" - "@blocksuite/blocks": "npm:0.11.0-nightly-202401020419-752a5b8" - "@blocksuite/global": "npm:0.11.0-nightly-202401020419-752a5b8" + "@blocksuite/block-std": "npm:0.12.0-nightly-202401120404-4219e86" + "@blocksuite/blocks": "npm:0.12.0-nightly-202401120404-4219e86" + "@blocksuite/global": "npm:0.12.0-nightly-202401120404-4219e86" "@blocksuite/icons": "npm:2.1.40" - "@blocksuite/inline": "npm:0.11.0-nightly-202401020419-752a5b8" - "@blocksuite/lit": "npm:0.11.0-nightly-202401020419-752a5b8" - "@blocksuite/presets": "npm:0.11.0-nightly-202401020419-752a5b8" - "@blocksuite/store": "npm:0.11.0-nightly-202401020419-752a5b8" + "@blocksuite/inline": "npm:0.12.0-nightly-202401120404-4219e86" + "@blocksuite/lit": "npm:0.12.0-nightly-202401120404-4219e86" + "@blocksuite/presets": "npm:0.12.0-nightly-202401120404-4219e86" + "@blocksuite/store": "npm:0.12.0-nightly-202401120404-4219e86" "@dnd-kit/sortable": "npm:^8.0.0" "@storybook/addon-actions": "npm:^7.5.3" "@storybook/addon-essentials": "npm:^7.5.3" @@ -3392,42 +3392,41 @@ __metadata: languageName: node linkType: hard -"@blocksuite/block-std@npm:0.11.0-nightly-202401020419-752a5b8": - version: 0.11.0-nightly-202401020419-752a5b8 - resolution: "@blocksuite/block-std@npm:0.11.0-nightly-202401020419-752a5b8" +"@blocksuite/block-std@npm:0.12.0-nightly-202401120404-4219e86": + version: 0.12.0-nightly-202401120404-4219e86 + resolution: "@blocksuite/block-std@npm:0.12.0-nightly-202401120404-4219e86" dependencies: - "@blocksuite/global": "npm:0.11.0-nightly-202401020419-752a5b8" + "@blocksuite/global": "npm:0.12.0-nightly-202401120404-4219e86" lz-string: "npm:^1.5.0" w3c-keyname: "npm:^2.2.8" zod: "npm:^3.22.4" peerDependencies: - "@blocksuite/store": 0.11.0-nightly-202401020419-752a5b8 - checksum: 98782705d13e981efd42143e7b104ad05fa57929d870fdc6d0964d8030f02f6d35cf9de7a6b47a13c8bfe6b2fbb2fdc5f43fd07bf3c3d795adf917e937d09ae2 + "@blocksuite/store": 0.12.0-nightly-202401120404-4219e86 + checksum: 370f1f940399fa4ce7c0700b8ccd2fc27d14a6778bac119311af826ac927e3c177c26de1e16568032dc4e0ce856b4c95a991813fbdae17531bf4b8f38038e17b languageName: node linkType: hard -"@blocksuite/blocks@npm:0.11.0-nightly-202401020419-752a5b8": - version: 0.11.0-nightly-202401020419-752a5b8 - resolution: "@blocksuite/blocks@npm:0.11.0-nightly-202401020419-752a5b8" +"@blocksuite/blocks@npm:0.12.0-nightly-202401120404-4219e86": + version: 0.12.0-nightly-202401120404-4219e86 + resolution: "@blocksuite/blocks@npm:0.12.0-nightly-202401120404-4219e86" dependencies: - "@blocksuite/block-std": "npm:0.11.0-nightly-202401020419-752a5b8" - "@blocksuite/global": "npm:0.11.0-nightly-202401020419-752a5b8" - "@blocksuite/inline": "npm:0.11.0-nightly-202401020419-752a5b8" - "@blocksuite/lit": "npm:0.11.0-nightly-202401020419-752a5b8" - "@blocksuite/store": "npm:0.11.0-nightly-202401020419-752a5b8" - "@floating-ui/dom": "npm:^1.5.3" + "@blocksuite/block-std": "npm:0.12.0-nightly-202401120404-4219e86" + "@blocksuite/global": "npm:0.12.0-nightly-202401120404-4219e86" + "@blocksuite/inline": "npm:0.12.0-nightly-202401120404-4219e86" + "@blocksuite/lit": "npm:0.12.0-nightly-202401120404-4219e86" + "@blocksuite/store": "npm:0.12.0-nightly-202401120404-4219e86" + "@floating-ui/dom": "npm:^1.5.4" "@toeverything/theme": "npm:^0.7.27" "@types/hast": "npm:^3.0.3" "@types/mdast": "npm:^4.0.3" "@types/webfontloader": "npm:^1.6.38" buffer: "npm:^6.0.3" - date-fns: "npm:^2.30.0" + date-fns: "npm:^3.1.0" file-type: "npm:^16.5.4" fractional-indexing: "npm:^3.2.0" html2canvas: "npm:^1.4.1" jszip: "npm:^3.10.1" lit: "npm:^3.1.0" - marked: "npm:^4.3.0" mdast-util-gfm-autolink-literal: "npm:^2.0.0" mdast-util-gfm-strikethrough: "npm:^2.0.0" mdast-util-gfm-table: "npm:^2.0.0" @@ -3444,21 +3443,21 @@ __metadata: rehype-stringify: "npm:^10.0.0" remark-parse: "npm:^11.0.0" remark-stringify: "npm:^11.0.0" - shiki: "npm:^0.14.5" + shiki: "npm:^0.14.7" sortablejs: "npm:^1.15.1" unified: "npm:^11.0.4" webfontloader: "npm:^1.6.28" zod: "npm:^3.22.4" - checksum: b8d1c625954a93667db96e54f410b0881a3fc87edd73383ea23baab515a5d102b3cbee57ddccc15964ebb39f0b5b39d6bf115a1ddbc6ffa23f161c6f42577dd3 + checksum: d26daebf5cdf9f863d5a03e72bf764d0c97975abe9f37acf38d55eb5070a722568ee4d044df5cfebe5ffe9785df1b1392194ee6c816ff74a5408ec54403e59b8 languageName: node linkType: hard -"@blocksuite/global@npm:0.11.0-nightly-202401020419-752a5b8": - version: 0.11.0-nightly-202401020419-752a5b8 - resolution: "@blocksuite/global@npm:0.11.0-nightly-202401020419-752a5b8" +"@blocksuite/global@npm:0.12.0-nightly-202401120404-4219e86": + version: 0.12.0-nightly-202401120404-4219e86 + resolution: "@blocksuite/global@npm:0.12.0-nightly-202401120404-4219e86" dependencies: zod: "npm:^3.22.4" - checksum: 4607e97f1c16f8976db43f6de0300a0a9cb54dc4846ff6659a200e9150127ed238688f9d2e3b92f4515e57c14ad1778e59988519f94e9ed080d0ef7faf2a15f3 + checksum: 247d7d6475936289720d8f7a4fa8b0dcb375c3a74115da4b8c9456d2df45a3e71ee073a2c0b5488cdf25dfe6b0eb9dfba21958e0a2066776c93850c292b904bd languageName: node linkType: hard @@ -3472,59 +3471,60 @@ __metadata: languageName: node linkType: hard -"@blocksuite/inline@npm:0.11.0-nightly-202401020419-752a5b8": - version: 0.11.0-nightly-202401020419-752a5b8 - resolution: "@blocksuite/inline@npm:0.11.0-nightly-202401020419-752a5b8" +"@blocksuite/inline@npm:0.12.0-nightly-202401120404-4219e86": + version: 0.12.0-nightly-202401120404-4219e86 + resolution: "@blocksuite/inline@npm:0.12.0-nightly-202401120404-4219e86" dependencies: - "@blocksuite/global": "npm:0.11.0-nightly-202401020419-752a5b8" + "@blocksuite/global": "npm:0.12.0-nightly-202401120404-4219e86" zod: "npm:^3.22.4" peerDependencies: lit: ^3.0.2 yjs: ^13 - checksum: 236c5452dbe8805c0daccf635676c6471a193bd22b912454f56ec8b61772524c6c29065932b9432d131213ea42875e7e6c5f91eaefcb3053a366db56d46abeb3 + checksum: 1c38558f5a7e9fac84a6130207618e2a75c29591c6dc264cd5e2eddee4a7dfc02964c66c8945ea4a8a6b3c468242e4c5b7d8ea639d77bd82cd5e59c87c5b1171 languageName: node linkType: hard -"@blocksuite/lit@npm:0.11.0-nightly-202401020419-752a5b8": - version: 0.11.0-nightly-202401020419-752a5b8 - resolution: "@blocksuite/lit@npm:0.11.0-nightly-202401020419-752a5b8" +"@blocksuite/lit@npm:0.12.0-nightly-202401120404-4219e86": + version: 0.12.0-nightly-202401120404-4219e86 + resolution: "@blocksuite/lit@npm:0.12.0-nightly-202401120404-4219e86" dependencies: - "@blocksuite/global": "npm:0.11.0-nightly-202401020419-752a5b8" - "@blocksuite/inline": "npm:0.11.0-nightly-202401020419-752a5b8" + "@blocksuite/global": "npm:0.12.0-nightly-202401120404-4219e86" + "@blocksuite/inline": "npm:0.12.0-nightly-202401120404-4219e86" lit: "npm:^3.1.0" peerDependencies: - "@blocksuite/block-std": 0.11.0-nightly-202401020419-752a5b8 - "@blocksuite/store": 0.11.0-nightly-202401020419-752a5b8 - checksum: 2b0e773200bfc0fd3507b728ebb8e9c72d62e80f0d7dd6a9fc49ac85a81b17f03a3552560e88d0c0c82a37ceb0c9a3beada5457db3052dac23ff4f65072a282c + "@blocksuite/block-std": 0.12.0-nightly-202401120404-4219e86 + "@blocksuite/store": 0.12.0-nightly-202401120404-4219e86 + checksum: 2b1df051a77f58376df1b88301041721588dd4b2f06c6bcde807316bdf8f1c311d53a9cb81254ef52a5e67bebc378cb4ea512ddefd16b1ad868c1d87fc47c958 languageName: node linkType: hard -"@blocksuite/presets@npm:0.11.0-nightly-202401020419-752a5b8": - version: 0.11.0-nightly-202401020419-752a5b8 - resolution: "@blocksuite/presets@npm:0.11.0-nightly-202401020419-752a5b8" +"@blocksuite/presets@npm:0.12.0-nightly-202401120404-4219e86": + version: 0.12.0-nightly-202401120404-4219e86 + resolution: "@blocksuite/presets@npm:0.12.0-nightly-202401120404-4219e86" dependencies: - "@blocksuite/block-std": "npm:0.11.0-nightly-202401020419-752a5b8" - "@blocksuite/blocks": "npm:0.11.0-nightly-202401020419-752a5b8" - "@blocksuite/global": "npm:0.11.0-nightly-202401020419-752a5b8" - "@blocksuite/inline": "npm:0.11.0-nightly-202401020419-752a5b8" - "@blocksuite/lit": "npm:0.11.0-nightly-202401020419-752a5b8" - "@blocksuite/store": "npm:0.11.0-nightly-202401020419-752a5b8" - "@fal-ai/serverless-client": "npm:^0.7.3" + "@blocksuite/block-std": "npm:0.12.0-nightly-202401120404-4219e86" + "@blocksuite/blocks": "npm:0.12.0-nightly-202401120404-4219e86" + "@blocksuite/global": "npm:0.12.0-nightly-202401120404-4219e86" + "@blocksuite/inline": "npm:0.12.0-nightly-202401120404-4219e86" + "@blocksuite/lit": "npm:0.12.0-nightly-202401120404-4219e86" + "@blocksuite/store": "npm:0.12.0-nightly-202401120404-4219e86" + "@fal-ai/serverless-client": "npm:^0.7.4" + "@floating-ui/dom": "npm:^1.5.4" "@toeverything/theme": "npm:^0.7.27" lit: "npm:^3.1.0" - openai: "npm:^4.20.1" - checksum: 8fed80f684a726018651ec9d95bd0afd99022e0cc2bebb376cf172f4c146ac86a5f8e4addf8bf24c8dece791d057e444f6d56e44692182c6b8b9bed099db06a0 + openai: "npm:^4.24.1" + checksum: b4d04692f34bd02cf312ca23aa03d294cc04d69e72d9b0a190d6c433aefc460aa37256ba8c8bead6fd900a0273831ae3e252d4ada9af48f4abcc96fa193b409a languageName: node linkType: hard -"@blocksuite/store@npm:0.11.0-nightly-202401020419-752a5b8": - version: 0.11.0-nightly-202401020419-752a5b8 - resolution: "@blocksuite/store@npm:0.11.0-nightly-202401020419-752a5b8" +"@blocksuite/store@npm:0.12.0-nightly-202401120404-4219e86": + version: 0.12.0-nightly-202401120404-4219e86 + resolution: "@blocksuite/store@npm:0.12.0-nightly-202401120404-4219e86" dependencies: - "@blocksuite/global": "npm:0.11.0-nightly-202401020419-752a5b8" - "@blocksuite/inline": "npm:0.11.0-nightly-202401020419-752a5b8" - "@types/flexsearch": "npm:^0.7.3" - flexsearch: "npm:0.7.21" + "@blocksuite/global": "npm:0.12.0-nightly-202401120404-4219e86" + "@blocksuite/inline": "npm:0.12.0-nightly-202401120404-4219e86" + "@types/flexsearch": "npm:^0.7.6" + flexsearch: "npm:0.7.43" idb-keyval: "npm:^6.2.1" lib0: "npm:^0.2.88" merge: "npm:^2.1.1" @@ -3535,7 +3535,7 @@ __metadata: peerDependencies: async-call-rpc: ^6 yjs: ^13 - checksum: 9f9d5ef99c48462f17d9aa399b0ea09e65989f2d24c28b4b6116ddea5dca964087692b0d2b76332009737ae33ed8a0559d7dc618ea3c280b7579522579080d30 + checksum: 8cd32c49fcaf9cefa4e4211cba392a9f1c8f1d14c1d601aa4758d4d4b88ca5cf2543032e28d486ef4f7de9a109d3c6b1eaca2bb5d5e81d772b1d702600674475 languageName: node linkType: hard @@ -5186,7 +5186,7 @@ __metadata: languageName: node linkType: hard -"@fal-ai/serverless-client@npm:^0.7.3": +"@fal-ai/serverless-client@npm:^0.7.4": version: 0.7.4 resolution: "@fal-ai/serverless-client@npm:0.7.4" dependencies: @@ -5231,7 +5231,16 @@ __metadata: languageName: node linkType: hard -"@floating-ui/dom@npm:^1.2.1, @floating-ui/dom@npm:^1.5.1, @floating-ui/dom@npm:^1.5.3": +"@floating-ui/core@npm:^1.5.3": + version: 1.5.3 + resolution: "@floating-ui/core@npm:1.5.3" + dependencies: + "@floating-ui/utils": "npm:^0.2.0" + checksum: 7d9feaca2565a2a71bf03d23cd292c03def63097d7fde7d62909cdb8ddb84664781f3922086bcf10443f3310cb92381a0ecf745b2774edb917fa74fe61015c56 + languageName: node + linkType: hard + +"@floating-ui/dom@npm:^1.2.1, @floating-ui/dom@npm:^1.5.1": version: 1.5.3 resolution: "@floating-ui/dom@npm:1.5.3" dependencies: @@ -5241,6 +5250,16 @@ __metadata: languageName: node linkType: hard +"@floating-ui/dom@npm:^1.5.4": + version: 1.5.4 + resolution: "@floating-ui/dom@npm:1.5.4" + dependencies: + "@floating-ui/core": "npm:^1.5.3" + "@floating-ui/utils": "npm:^0.2.0" + checksum: 3ba02ba2b4227c1e18df6ccdd029a1c100058db2e76ca1dac60a593ec72b2d4d995fa5c2d1639a5c38adb17e12398fbfe4f6cf5fd45f2ee6170ed0cf64acea06 + languageName: node + linkType: hard + "@floating-ui/react-dom@npm:^1.3.0": version: 1.3.0 resolution: "@floating-ui/react-dom@npm:1.3.0" @@ -5286,6 +5305,13 @@ __metadata: languageName: node linkType: hard +"@floating-ui/utils@npm:^0.2.0": + version: 0.2.1 + resolution: "@floating-ui/utils@npm:0.2.1" + checksum: 33c9ab346e7b05c5a1e6a95bc902aafcfc2c9d513a147e2491468843bd5607531b06d0b9aa56aa491cbf22a6c2495c18ccfc4c0344baec54a689a7bb8e4898d6 + languageName: node + linkType: hard + "@gar/promisify@npm:^1.1.3": version: 1.1.3 resolution: "@gar/promisify@npm:1.1.3" @@ -12814,11 +12840,11 @@ __metadata: "@affine/debug": "workspace:*" "@affine/env": "workspace:*" "@affine/templates": "workspace:*" - "@blocksuite/blocks": "npm:0.11.0-nightly-202401020419-752a5b8" - "@blocksuite/global": "npm:0.11.0-nightly-202401020419-752a5b8" - "@blocksuite/lit": "npm:0.11.0-nightly-202401020419-752a5b8" - "@blocksuite/presets": "npm:0.11.0-nightly-202401020419-752a5b8" - "@blocksuite/store": "npm:0.11.0-nightly-202401020419-752a5b8" + "@blocksuite/blocks": "npm:0.12.0-nightly-202401120404-4219e86" + "@blocksuite/global": "npm:0.12.0-nightly-202401120404-4219e86" + "@blocksuite/lit": "npm:0.12.0-nightly-202401120404-4219e86" + "@blocksuite/presets": "npm:0.12.0-nightly-202401120404-4219e86" + "@blocksuite/store": "npm:0.12.0-nightly-202401120404-4219e86" async-call-rpc: "npm:^6.3.1" jotai: "npm:^2.5.1" jotai-effect: "npm:^0.2.3" @@ -12865,9 +12891,9 @@ __metadata: version: 0.0.0-use.local resolution: "@toeverything/y-indexeddb@workspace:packages/common/y-indexeddb" dependencies: - "@blocksuite/blocks": "npm:0.11.0-nightly-202401020419-752a5b8" - "@blocksuite/global": "npm:0.11.0-nightly-202401020419-752a5b8" - "@blocksuite/store": "npm:0.11.0-nightly-202401020419-752a5b8" + "@blocksuite/blocks": "npm:0.12.0-nightly-202401120404-4219e86" + "@blocksuite/global": "npm:0.12.0-nightly-202401120404-4219e86" + "@blocksuite/store": "npm:0.12.0-nightly-202401120404-4219e86" fake-indexeddb: "npm:^5.0.0" idb: "npm:^8.0.0" nanoid: "npm:^5.0.3" @@ -13287,7 +13313,7 @@ __metadata: languageName: node linkType: hard -"@types/flexsearch@npm:^0.7.3": +"@types/flexsearch@npm:^0.7.6": version: 0.7.6 resolution: "@types/flexsearch@npm:0.7.6" checksum: 93aaed91f5847a0c85e19b339147067897c92385de842071c959bf3f1efb8c7095acce460385aa285c75c67022d78979b4cae6ecb754bdb74348bf4976160ae3 @@ -18163,6 +18189,13 @@ __metadata: languageName: node linkType: hard +"date-fns@npm:^3.1.0": + version: 3.2.0 + resolution: "date-fns@npm:3.2.0" + checksum: 2f36cb9165a066ae90c0b6f0c25b3c70eb75bc52e64ba6dd8680964ebda144851a244ff4ed0b2afb4a6bab796a1af6a0e6c7c2814529d04c72fa434f931d4e81 + languageName: node + linkType: hard + "date-time@npm:^3.1.0": version: 3.1.0 resolution: "date-time@npm:3.1.0" @@ -20778,10 +20811,10 @@ __metadata: languageName: node linkType: hard -"flexsearch@npm:0.7.21": - version: 0.7.21 - resolution: "flexsearch@npm:0.7.21" - checksum: af0c88fb76817a8b8a35c63068bb85212e43f4da968e3480620ca5ad14eed458e757ab4fae70f7a07ba04de1a6dbf531ad27a5c489b5a2b629ba817f2d8e9cd4 +"flexsearch@npm:0.7.43": + version: 0.7.43 + resolution: "flexsearch@npm:0.7.43" + checksum: 79c0bceee435bd40fd0a0d8ba72f1bd07f622374a73bd3ceb874f7c7dd9a0fe9863b5c66a4037c2fa534e726f718d010ac99377d1924e347823b51ce641180e5 languageName: node linkType: hard @@ -28017,9 +28050,9 @@ __metadata: languageName: node linkType: hard -"openai@npm:^4.20.1": - version: 4.20.1 - resolution: "openai@npm:4.20.1" +"openai@npm:^4.24.1": + version: 4.24.4 + resolution: "openai@npm:4.24.4" dependencies: "@types/node": "npm:^18.11.18" "@types/node-fetch": "npm:^2.6.4" @@ -28032,7 +28065,7 @@ __metadata: web-streams-polyfill: "npm:^3.2.1" bin: openai: bin/cli - checksum: c80c08c9cc39e64114e8bd41f8580223638476aa9fe5e11a5b505128350fcceb1fe6886004c9b37431b4520bec91815e897f545335b24f8f15bdd69c7185af45 + checksum: d3c83b03a5680fb9fd539b08c27b7b66ab405b400b068c18a71714b8f241abe6acaf957dfb733485a497e418e3c0488d4ef43e219d9fdffa0146bb383fcacb8a languageName: node linkType: hard @@ -31496,7 +31529,7 @@ __metadata: languageName: node linkType: hard -"shiki@npm:^0.14.1, shiki@npm:^0.14.5": +"shiki@npm:^0.14.1, shiki@npm:^0.14.7": version: 0.14.7 resolution: "shiki@npm:0.14.7" dependencies: @@ -35255,7 +35288,7 @@ __metadata: version: 0.0.0-use.local resolution: "y-provider@workspace:packages/common/y-provider" dependencies: - "@blocksuite/store": "npm:0.11.0-nightly-202401020419-752a5b8" + "@blocksuite/store": "npm:0.12.0-nightly-202401120404-4219e86" vite: "npm:^5.0.6" vite-plugin-dts: "npm:3.7.0" vitest: "npm:1.1.3" From 5aea84af8fbb6cdbc9a01ad6265928986f7d772b Mon Sep 17 00:00:00 2001 From: Peng Xiao Date: Fri, 12 Jan 2024 16:48:55 +0800 Subject: [PATCH 12/92] fix(electron): remove cors headers hack (#5581) --- packages/frontend/electron/src/main/protocol.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/packages/frontend/electron/src/main/protocol.ts b/packages/frontend/electron/src/main/protocol.ts index e874ff6cab..2af632983c 100644 --- a/packages/frontend/electron/src/main/protocol.ts +++ b/packages/frontend/electron/src/main/protocol.ts @@ -79,16 +79,6 @@ export function registerProtocol() { (responseDetails, callback) => { const { responseHeaders } = responseDetails; if (responseHeaders) { - delete responseHeaders['access-control-allow-origin']; - delete responseHeaders['access-control-allow-methods']; - responseHeaders['Access-Control-Allow-Origin'] = ['*']; - responseHeaders['Access-Control-Allow-Methods'] = [ - 'GET', - 'POST', - 'PUT', - 'DELETE', - 'OPTIONS', - ]; // replace SameSite=Lax with SameSite=None const originalCookie = responseHeaders['set-cookie'] || responseHeaders['Set-Cookie']; From 18907ebe573dfa6da251af6a44aa3679e9ebde30 Mon Sep 17 00:00:00 2001 From: DarkSky <25152247+darkskygit@users.noreply.github.com> Date: Sat, 13 Jan 2024 23:26:55 +0800 Subject: [PATCH 13/92] fix: return empty resp if user not exists in login preflight (#5588) --- packages/backend/server/src/modules/users/resolver.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/backend/server/src/modules/users/resolver.ts b/packages/backend/server/src/modules/users/resolver.ts index 12842820ae..eaedd169e0 100644 --- a/packages/backend/server/src/modules/users/resolver.ts +++ b/packages/backend/server/src/modules/users/resolver.ts @@ -112,6 +112,9 @@ export class UserResolver { const user = await this.users.findUserByEmail(email); if (currentUser) return user; + // return empty response when user not exists + if (!user) return null; + // only return limited info when not logged in return { email: user?.email, From 4c49b62ab700a1b81e966d80f6f304c789b9a80a Mon Sep 17 00:00:00 2001 From: liuyi Date: Sun, 14 Jan 2024 05:47:56 +0000 Subject: [PATCH 14/92] fix(server): node imports order (#5583) --- packages/backend/server/package.json | 4 +- packages/backend/server/src/app.module.ts | 38 ++++++++++ packages/backend/server/src/app.ts | 74 ++++++++++--------- packages/backend/server/src/data/app.ts | 19 +---- packages/backend/server/src/data/index.ts | 16 ++++ .../server/src/fundamentals/config/index.ts | 2 - packages/backend/server/src/index.ts | 40 +--------- packages/backend/server/tests/app.e2e.ts | 2 +- packages/backend/server/tests/mailer.spec.ts | 2 +- packages/backend/server/tests/sync.spec.ts | 2 +- packages/backend/server/tests/user.e2e.ts | 2 +- packages/backend/server/tests/utils/utils.ts | 2 +- .../server/tests/workspace-blobs.spec.ts | 2 +- .../server/tests/workspace-invite.e2e.ts | 2 +- .../backend/server/tests/workspace.e2e.ts | 2 +- packages/frontend/graphql/src/schema.ts | 1 + 16 files changed, 109 insertions(+), 101 deletions(-) create mode 100644 packages/backend/server/src/app.module.ts create mode 100644 packages/backend/server/src/data/index.ts diff --git a/packages/backend/server/package.json b/packages/backend/server/package.json index 7b85432795..cffb49d4d8 100644 --- a/packages/backend/server/package.json +++ b/packages/backend/server/package.json @@ -14,8 +14,8 @@ "test": "ava --concurrency 1 --serial", "test:coverage": "c8 ava --concurrency 1 --serial", "postinstall": "prisma generate", - "data-migration": "node --loader ts-node/esm/transpile-only.mjs --es-module-specifier-resolution node ./src/data/app.ts", - "predeploy": "yarn prisma migrate deploy && node --es-module-specifier-resolution node ./dist/data/app.js run" + "data-migration": "node --loader ts-node/esm/transpile-only.mjs --es-module-specifier-resolution node ./src/data/index.ts", + "predeploy": "yarn prisma migrate deploy && node --es-module-specifier-resolution node ./dist/data/index.js run" }, "dependencies": { "@apollo/server": "^4.9.5", diff --git a/packages/backend/server/src/app.module.ts b/packages/backend/server/src/app.module.ts new file mode 100644 index 0000000000..8efc1b1f32 --- /dev/null +++ b/packages/backend/server/src/app.module.ts @@ -0,0 +1,38 @@ +import { DynamicModule, Module, Type } from '@nestjs/common'; +import { APP_INTERCEPTOR } from '@nestjs/core'; + +import { AppController } from './app.controller'; +import { CacheInterceptor, CacheModule } from './fundamentals/cache'; +import { ConfigModule } from './fundamentals/config'; +import { EventModule } from './fundamentals/event'; +import { MailModule } from './fundamentals/mailer'; +import { MetricsModule } from './fundamentals/metrics'; +import { PrismaModule } from './fundamentals/prisma'; +import { SessionModule } from './fundamentals/session'; +import { RateLimiterModule } from './fundamentals/throttler'; +import { BusinessModules } from './modules'; +import { AuthModule } from './modules/auth'; + +export const FunctionalityModules: Array = [ + ConfigModule.forRoot(), + CacheModule, + PrismaModule, + MetricsModule, + EventModule, + SessionModule, + RateLimiterModule, + AuthModule, + MailModule, +]; + +@Module({ + providers: [ + { + provide: APP_INTERCEPTOR, + useClass: CacheInterceptor, + }, + ], + imports: [...FunctionalityModules, ...BusinessModules], + controllers: [AppController], +}) +export class AppModule {} diff --git a/packages/backend/server/src/app.ts b/packages/backend/server/src/app.ts index 8efc1b1f32..c3c8edbcd3 100644 --- a/packages/backend/server/src/app.ts +++ b/packages/backend/server/src/app.ts @@ -1,38 +1,42 @@ -import { DynamicModule, Module, Type } from '@nestjs/common'; -import { APP_INTERCEPTOR } from '@nestjs/core'; +import { NestFactory } from '@nestjs/core'; +import type { NestExpressApplication } from '@nestjs/platform-express'; +import cookieParser from 'cookie-parser'; +import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs'; -import { AppController } from './app.controller'; -import { CacheInterceptor, CacheModule } from './fundamentals/cache'; -import { ConfigModule } from './fundamentals/config'; -import { EventModule } from './fundamentals/event'; -import { MailModule } from './fundamentals/mailer'; -import { MetricsModule } from './fundamentals/metrics'; -import { PrismaModule } from './fundamentals/prisma'; -import { SessionModule } from './fundamentals/session'; -import { RateLimiterModule } from './fundamentals/throttler'; -import { BusinessModules } from './modules'; -import { AuthModule } from './modules/auth'; +import { CacheRedis } from './fundamentals/cache/redis'; +import { RedisIoAdapter } from './fundamentals/websocket'; +import { ExceptionLogger } from './middleware/exception-logger'; +import { serverTimingAndCache } from './middleware/timing'; -export const FunctionalityModules: Array = [ - ConfigModule.forRoot(), - CacheModule, - PrismaModule, - MetricsModule, - EventModule, - SessionModule, - RateLimiterModule, - AuthModule, - MailModule, -]; +export async function createApp() { + const { AppModule } = await import('./app.module'); -@Module({ - providers: [ - { - provide: APP_INTERCEPTOR, - useClass: CacheInterceptor, - }, - ], - imports: [...FunctionalityModules, ...BusinessModules], - controllers: [AppController], -}) -export class AppModule {} + const app = await NestFactory.create(AppModule, { + cors: true, + rawBody: true, + bodyParser: true, + logger: AFFiNE.affine.stable ? ['log'] : ['verbose'], + }); + + app.use(serverTimingAndCache); + + app.use( + graphqlUploadExpress({ + // TODO: dynamic limit by quota + maxFileSize: 100 * 1024 * 1024, + maxFiles: 5, + }) + ); + + app.useGlobalFilters(new ExceptionLogger()); + app.use(cookieParser()); + + if (AFFiNE.redis.enabled) { + const redis = app.get(CacheRedis, { strict: false }); + const redisIoAdapter = new RedisIoAdapter(app); + await redisIoAdapter.connectToRedis(redis); + app.useWebSocketAdapter(redisIoAdapter); + } + + return app; +} diff --git a/packages/backend/server/src/data/app.ts b/packages/backend/server/src/data/app.ts index 84c3027a3d..9fd4c58864 100644 --- a/packages/backend/server/src/data/app.ts +++ b/packages/backend/server/src/data/app.ts @@ -1,9 +1,6 @@ -import '../prelude'; +import { Module } from '@nestjs/common'; -import { Logger, Module } from '@nestjs/common'; -import { CommandFactory } from 'nest-commander'; - -import { AppModule as BusinessAppModule } from '../app'; +import { AppModule as BusinessAppModule } from '../app.module'; import { ConfigModule } from '../fundamentals/config'; import { CreateCommand, NameQuestion } from './commands/create'; import { RevertCommand, RunCommand } from './commands/run'; @@ -21,14 +18,4 @@ import { RevertCommand, RunCommand } from './commands/run'; ], providers: [NameQuestion, CreateCommand, RunCommand, RevertCommand], }) -class AppModule {} - -async function bootstrap() { - await CommandFactory.run(AppModule, new Logger()).catch(e => { - console.error(e); - process.exit(1); - }); - process.exit(0); -} - -await bootstrap(); +export class CliAppModule {} diff --git a/packages/backend/server/src/data/index.ts b/packages/backend/server/src/data/index.ts new file mode 100644 index 0000000000..2d2f324f52 --- /dev/null +++ b/packages/backend/server/src/data/index.ts @@ -0,0 +1,16 @@ +import '../prelude'; + +import { Logger } from '@nestjs/common'; +import { CommandFactory } from 'nest-commander'; + +import { CliAppModule } from './app'; + +async function bootstrap() { + await CommandFactory.run(CliAppModule, new Logger()).catch(e => { + console.error(e); + process.exit(1); + }); + process.exit(0); +} + +await bootstrap(); diff --git a/packages/backend/server/src/fundamentals/config/index.ts b/packages/backend/server/src/fundamentals/config/index.ts index 34fadc0421..9936b06f66 100644 --- a/packages/backend/server/src/fundamentals/config/index.ts +++ b/packages/backend/server/src/fundamentals/config/index.ts @@ -1,5 +1,3 @@ -// eslint-disable-next-line simple-import-sort/imports - export * from './def'; export * from './default'; export { applyEnvToConfig, parseEnvValue } from './env'; diff --git a/packages/backend/server/src/index.ts b/packages/backend/server/src/index.ts index 60a8ce3fb8..8499eec2d4 100644 --- a/packages/backend/server/src/index.ts +++ b/packages/backend/server/src/index.ts @@ -2,45 +2,9 @@ // keep the config import at the top // eslint-disable-next-line simple-import-sort/imports import './prelude'; +import { createApp } from './app'; -import { NestFactory } from '@nestjs/core'; -import type { NestExpressApplication } from '@nestjs/platform-express'; -import cookieParser from 'cookie-parser'; -import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs'; - -import { AppModule } from './app'; -import { RedisIoAdapter } from './fundamentals'; -import { CacheRedis } from './fundamentals/cache/redis'; -import { ExceptionLogger } from './middleware/exception-logger'; -import { serverTimingAndCache } from './middleware/timing'; - -const app = await NestFactory.create(AppModule, { - cors: true, - rawBody: true, - bodyParser: true, - logger: AFFiNE.affine.stable ? ['log'] : ['verbose'], -}); - -app.use(serverTimingAndCache); - -app.use( - graphqlUploadExpress({ - // TODO: dynamic limit by quota - maxFileSize: 100 * 1024 * 1024, - maxFiles: 5, - }) -); - -app.useGlobalFilters(new ExceptionLogger()); -app.use(cookieParser()); - -if (AFFiNE.redis.enabled) { - const redis = app.get(CacheRedis, { strict: false }); - const redisIoAdapter = new RedisIoAdapter(app); - await redisIoAdapter.connectToRedis(redis); - app.useWebSocketAdapter(redisIoAdapter); -} - +const app = await createApp(); await app.listen(AFFiNE.port, AFFiNE.host); console.log( diff --git a/packages/backend/server/tests/app.e2e.ts b/packages/backend/server/tests/app.e2e.ts index acd38d68e5..8a85bff345 100644 --- a/packages/backend/server/tests/app.e2e.ts +++ b/packages/backend/server/tests/app.e2e.ts @@ -9,7 +9,7 @@ import ava, { type TestFn } from 'ava'; import type { Express } from 'express'; import request from 'supertest'; -import { AppModule } from '../src/app'; +import { AppModule } from '../src/app.module'; import { FeatureManagementService } from '../src/modules/features'; import { createTestingApp } from './utils'; diff --git a/packages/backend/server/tests/mailer.spec.ts b/packages/backend/server/tests/mailer.spec.ts index b01bcfb931..6afa234e56 100644 --- a/packages/backend/server/tests/mailer.spec.ts +++ b/packages/backend/server/tests/mailer.spec.ts @@ -6,7 +6,7 @@ import { type User } from '@prisma/client'; import { PrismaClient } from '@prisma/client'; import ava, { type TestFn } from 'ava'; -import { AppModule } from '../src/app'; +import { AppModule } from '../src/app.module'; import { MailService } from '../src/fundamentals/mailer'; import { FeatureKind, FeatureManagementService } from '../src/modules/features'; import { Quotas } from '../src/modules/quota'; diff --git a/packages/backend/server/tests/sync.spec.ts b/packages/backend/server/tests/sync.spec.ts index fd15d0739d..d384199c88 100644 --- a/packages/backend/server/tests/sync.spec.ts +++ b/packages/backend/server/tests/sync.spec.ts @@ -4,7 +4,7 @@ import test from 'ava'; test('should be able to bootstrap sync server', async t => { // set env before import process.env.SERVER_FLAVOR = 'sync'; - const { AppModule } = await import('../src/app'); + const { AppModule } = await import('../src/app.module'); await t.notThrowsAsync(async () => { const module = await Test.createTestingModule({ imports: [AppModule], diff --git a/packages/backend/server/tests/user.e2e.ts b/packages/backend/server/tests/user.e2e.ts index 3932282a44..f4c61909a6 100644 --- a/packages/backend/server/tests/user.e2e.ts +++ b/packages/backend/server/tests/user.e2e.ts @@ -2,7 +2,7 @@ import type { INestApplication } from '@nestjs/common'; import test from 'ava'; import request from 'supertest'; -import { AppModule } from '../src/app'; +import { AppModule } from '../src/app.module'; import { createTestingApp, currentUser, signUp } from './utils'; let app: INestApplication; diff --git a/packages/backend/server/tests/utils/utils.ts b/packages/backend/server/tests/utils/utils.ts index fcfd25c3b6..55d3d23e9d 100644 --- a/packages/backend/server/tests/utils/utils.ts +++ b/packages/backend/server/tests/utils/utils.ts @@ -4,7 +4,7 @@ import { Test, TestingModuleBuilder } from '@nestjs/testing'; import { PrismaClient } from '@prisma/client'; import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs'; -import { AppModule, FunctionalityModules } from '../../src/app'; +import { AppModule, FunctionalityModules } from '../../src/app.module'; import { UserFeaturesInit1698652531198 } from '../../src/data/migrations/1698652531198-user-features-init'; import { GqlModule } from '../../src/fundamentals/graphql'; diff --git a/packages/backend/server/tests/workspace-blobs.spec.ts b/packages/backend/server/tests/workspace-blobs.spec.ts index 4a1969ed59..af0a341f11 100644 --- a/packages/backend/server/tests/workspace-blobs.spec.ts +++ b/packages/backend/server/tests/workspace-blobs.spec.ts @@ -2,7 +2,7 @@ import type { INestApplication } from '@nestjs/common'; import test from 'ava'; import request from 'supertest'; -import { AppModule } from '../src/app'; +import { AppModule } from '../src/app.module'; import { FeatureManagementService, FeatureType } from '../src/modules/features'; import { QuotaService, QuotaType } from '../src/modules/quota'; import { diff --git a/packages/backend/server/tests/workspace-invite.e2e.ts b/packages/backend/server/tests/workspace-invite.e2e.ts index 60e43175b0..2e9a9e78cf 100644 --- a/packages/backend/server/tests/workspace-invite.e2e.ts +++ b/packages/backend/server/tests/workspace-invite.e2e.ts @@ -6,7 +6,7 @@ import type { INestApplication } from '@nestjs/common'; import { PrismaClient } from '@prisma/client'; import ava, { type TestFn } from 'ava'; -import { AppModule } from '../src/app'; +import { AppModule } from '../src/app.module'; import { MailService } from '../src/fundamentals/mailer'; import { AuthService } from '../src/modules/auth/service'; import { diff --git a/packages/backend/server/tests/workspace.e2e.ts b/packages/backend/server/tests/workspace.e2e.ts index 4ea4e592eb..45a68ea6c5 100644 --- a/packages/backend/server/tests/workspace.e2e.ts +++ b/packages/backend/server/tests/workspace.e2e.ts @@ -3,7 +3,7 @@ import { PrismaClient } from '@prisma/client'; import ava, { type TestFn } from 'ava'; import request from 'supertest'; -import { AppModule } from '../src/app'; +import { AppModule } from '../src/app.module'; import { acceptInviteById, createTestingApp, diff --git a/packages/frontend/graphql/src/schema.ts b/packages/frontend/graphql/src/schema.ts index 5a279e5d43..cba95af545 100644 --- a/packages/frontend/graphql/src/schema.ts +++ b/packages/frontend/graphql/src/schema.ts @@ -36,6 +36,7 @@ export interface Scalars { export enum FeatureType { Copilot = 'Copilot', EarlyAccess = 'EarlyAccess', + UnlimitedWorkspace = 'UnlimitedWorkspace', } export enum InvoiceStatus { From 2f9b4fd0cf16bc089b7bdabfc299061c5b4522d5 Mon Sep 17 00:00:00 2001 From: DarkSky Date: Mon, 15 Jan 2024 08:33:05 +0000 Subject: [PATCH 15/92] fix: add field polyfill for old feature (#5586) --- .../server/src/modules/features/types/early-access.ts | 5 ++++- packages/backend/server/src/modules/features/types/index.ts | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/backend/server/src/modules/features/types/early-access.ts b/packages/backend/server/src/modules/features/types/early-access.ts index abf3b238f5..f8624b065a 100644 --- a/packages/backend/server/src/modules/features/types/early-access.ts +++ b/packages/backend/server/src/modules/features/types/early-access.ts @@ -4,5 +4,8 @@ import { FeatureType } from './common'; export const featureEarlyAccess = z.object({ feature: z.literal(FeatureType.EarlyAccess), - configs: z.object({}), + configs: z.object({ + // field polyfill, make it optional in the future + whitelist: z.string().array(), + }), }); diff --git a/packages/backend/server/src/modules/features/types/index.ts b/packages/backend/server/src/modules/features/types/index.ts index 0f82a393aa..f732bce242 100644 --- a/packages/backend/server/src/modules/features/types/index.ts +++ b/packages/backend/server/src/modules/features/types/index.ts @@ -42,7 +42,9 @@ export const Features: Feature[] = [ feature: FeatureType.EarlyAccess, type: FeatureKind.Feature, version: 2, - configs: {}, + configs: { + whitelist: [], + }, }, { feature: FeatureType.UnlimitedWorkspace, From 24e18dd4756a26760489a6f6bcf607cb90fa4b51 Mon Sep 17 00:00:00 2001 From: liuyi Date: Mon, 15 Jan 2024 09:24:52 +0000 Subject: [PATCH 16/92] fix: improve self-host convenience (#5582) --- .github/deployment/self-host/compose.yaml | 28 +++++----- packages/backend/server/.env.example | 12 ++--- .../server/scripts/self-host-predeploy.js | 51 +++++++++++++++++++ packages/backend/server/src/app.controller.ts | 1 + .../1605053000403-self-host-admin.ts | 39 ++++++++++++++ .../1702620653283-old-user-feature.ts | 1 - .../server/src/fundamentals/storage/index.ts | 2 +- packages/backend/server/src/prelude.ts | 24 +++++++-- 8 files changed, 132 insertions(+), 26 deletions(-) create mode 100644 packages/backend/server/scripts/self-host-predeploy.js create mode 100644 packages/backend/server/src/data/migrations/1605053000403-self-host-admin.ts diff --git a/.github/deployment/self-host/compose.yaml b/.github/deployment/self-host/compose.yaml index cf10de9318..1baa556f48 100644 --- a/.github/deployment/self-host/compose.yaml +++ b/.github/deployment/self-host/compose.yaml @@ -1,13 +1,9 @@ services: affine: image: ghcr.io/toeverything/affine-graphql:beta - container_name: affine + container_name: affine_selfhosted command: - [ - 'sh', - '-c', - './node_modules/.bin/dotenv -e /root/.affine/.env -- npm run predeploy && node --es-module-specifier-resolution=node ./dist/index.js', - ] + ['sh', '-c', 'node ./scripts/self-host-predeploy && node ./dist/index.js'] ports: - '3010:3010' - '5555:5555' @@ -17,23 +13,31 @@ services: postgres: condition: service_healthy volumes: - - ~/.affine/storage:/root/.affine/storage - - ~/.affine/.env:/root/.affine/.env + # custom configurations + - ~/.affine/self-host/config:/root/.affine/config + # blob storage + - ~/.affine/self-host/storage:/root/.affine/storage logging: driver: 'json-file' options: max-size: '1000m' restart: unless-stopped environment: + - NODE_OPTIONS=--es-module-specifier-resolution node + - AFFINE_CONFIG_PATH=/root/.affine/config + - REDIS_SERVER_HOST=redis + - DATABASE_URL=postgres://affine:affine@postgres:5432/affine - DISABLE_TELEMETRY=true - NODE_ENV=production - SERVER_FLAVOR=selfhosted + - AFFINE_ADMIN_EMAIL=${AFFINE_ADMIN_EMAIL} + - AFFINE_ADMIN_PASSWORD=${AFFINE_ADMIN_PASSWORD} redis: image: redis - container_name: redis + container_name: affine_redis restart: unless-stopped volumes: - - ~/.affine/redis:/data + - ~/.affine/self-host/redis:/data healthcheck: test: ['CMD', 'redis-cli', '--raw', 'incr', 'ping'] interval: 10s @@ -41,10 +45,10 @@ services: retries: 5 postgres: image: postgres - container_name: postgres + container_name: affine_postgres restart: unless-stopped volumes: - - ~/.affine/postgres:/var/lib/postgresql/data + - ~/.affine/self-host/postgres:/var/lib/postgresql/data healthcheck: test: ['CMD-SHELL', 'pg_isready -U affine'] interval: 10s diff --git a/packages/backend/server/.env.example b/packages/backend/server/.env.example index 749873dc40..b3690ad7f7 100644 --- a/packages/backend/server/.env.example +++ b/packages/backend/server/.env.example @@ -1,8 +1,4 @@ -DATABASE_URL="postgresql://affine@localhost:5432/affine" -NEXTAUTH_URL="http://localhost:8080" -OAUTH_EMAIL_SENDER="noreply@toeverything.info" -OAUTH_EMAIL_LOGIN="" -OAUTH_EMAIL_PASSWORD="" -ENABLE_LOCAL_EMAIL="true" -STRIPE_API_KEY= -STRIPE_WEBHOOK_KEY= +# AFFINE_SERVER_PORT=3010 +# AFFINE_SERVER_HOST=app.affine.pro +# AFFINE_SERVER_HTTPS=true +# DATABASE_URL="postgres://affine@localhost:5432/affine" diff --git a/packages/backend/server/scripts/self-host-predeploy.js b/packages/backend/server/scripts/self-host-predeploy.js new file mode 100644 index 0000000000..bcd618a80f --- /dev/null +++ b/packages/backend/server/scripts/self-host-predeploy.js @@ -0,0 +1,51 @@ +import { execSync } from 'node:child_process'; +import fs from 'node:fs'; +import path from 'node:path'; + +const SELF_HOST_CONFIG_DIR = '/root/.affine/config'; +/** + * @type {Array<{ from: string; to?: string, modifier?: (content: string): string }>} + */ +const configFiles = [ + { from: './.env.example', to: '.env' }, + { from: './dist/config/affine.js', modifier: configCleaner }, + { from: './dist/config/affine.env.js', modifier: configCleaner }, +]; + +function configCleaner(content) { + return content.replace(/(\/\/#.*$)|(\/\/\s+TODO.*$)/gm, ''); +} + +function prepare() { + fs.mkdirSync(SELF_HOST_CONFIG_DIR, { recursive: true }); + + for (const { from, to, modifier } of configFiles) { + const targetFileName = to ?? path.parse(from).base; + const targetFilePath = path.join(SELF_HOST_CONFIG_DIR, targetFileName); + if (!fs.existsSync(targetFilePath)) { + console.log(`creating config file [${targetFilePath}].`); + if (modifier) { + const content = fs.readFileSync(from, 'utf-8'); + fs.writeFileSync(targetFilePath, modifier(content), 'utf-8'); + } else { + fs.cpSync(from, targetFilePath, { + force: false, + }); + } + } + } +} + +function runPredeployScript() { + console.log('running predeploy script.'); + execSync('yarn predeploy', { + env: { + ...process.env, + NODE_OPTIONS: + (process.env.NODE_OPTIONS ?? '') + ' --import ./dist/prelude.js', + }, + }); +} + +prepare(); +runPredeployScript(); diff --git a/packages/backend/server/src/app.controller.ts b/packages/backend/server/src/app.controller.ts index 87f3f981df..0e3a4abbf2 100644 --- a/packages/backend/server/src/app.controller.ts +++ b/packages/backend/server/src/app.controller.ts @@ -11,6 +11,7 @@ export class AppController { return { compatibility: this.config.version, message: `AFFiNE ${this.config.version} Server`, + flavor: this.config.flavor, }; } } diff --git a/packages/backend/server/src/data/migrations/1605053000403-self-host-admin.ts b/packages/backend/server/src/data/migrations/1605053000403-self-host-admin.ts new file mode 100644 index 0000000000..0acd6c1c81 --- /dev/null +++ b/packages/backend/server/src/data/migrations/1605053000403-self-host-admin.ts @@ -0,0 +1,39 @@ +import { ModuleRef } from '@nestjs/core'; +import { hash } from '@node-rs/argon2'; +import { PrismaClient } from '@prisma/client'; + +import { Config } from '../../fundamentals'; + +export class SelfHostAdmin1605053000403 { + // do the migration + static async up(db: PrismaClient, ref: ModuleRef) { + const config = ref.get(Config, { strict: false }); + if (config.flavor === 'selfhosted') { + if ( + !process.env.AFFINE_ADMIN_EMAIL || + !process.env.AFFINE_ADMIN_PASSWORD + ) { + throw new Error( + 'You have to set AFFINE_ADMIN_EMAIL and AFFINE_ADMIN_PASSWORD environment variables to generate the initial user for self-hosted AFFiNE Server.' + ); + } + await db.user.create({ + data: { + name: 'AFFINE First User', + email: process.env.AFFINE_ADMIN_EMAIL, + emailVerified: new Date(), + password: await hash(process.env.AFFINE_ADMIN_PASSWORD), + }, + }); + } + } + + // revert the migration + static async down(db: PrismaClient) { + await db.user.deleteMany({ + where: { + email: process.env.AFFINE_ADMIN_EMAIL ?? 'admin@example.com', + }, + }); + } +} diff --git a/packages/backend/server/src/data/migrations/1702620653283-old-user-feature.ts b/packages/backend/server/src/data/migrations/1702620653283-old-user-feature.ts index 2656ab814c..9cbce5a0a8 100644 --- a/packages/backend/server/src/data/migrations/1702620653283-old-user-feature.ts +++ b/packages/backend/server/src/data/migrations/1702620653283-old-user-feature.ts @@ -16,7 +16,6 @@ export class OldUserFeature1702620653283 { where: { NOT: { features: { some: { NOT: { id: { gt: 0 } } } } } }, select: { id: true }, }); - console.log(`migrating ${userIds.join('|')} users`); await tx.userFeatures.createMany({ data: userIds.map(({ id: userId }) => ({ diff --git a/packages/backend/server/src/fundamentals/storage/index.ts b/packages/backend/server/src/fundamentals/storage/index.ts index d2ecdf9284..fe73e30463 100644 --- a/packages/backend/server/src/fundamentals/storage/index.ts +++ b/packages/backend/server/src/fundamentals/storage/index.ts @@ -9,7 +9,7 @@ try { const require = createRequire(import.meta.url); storageModule = process.arch === 'arm64' - ? require('../.././storage.arm64.node') + ? require('../../../storage.arm64.node') : process.arch === 'arm' ? require('../../../storage.armv7.node') : require('../../../storage.node'); diff --git a/packages/backend/server/src/prelude.ts b/packages/backend/server/src/prelude.ts index 22a6315fed..42203d3633 100644 --- a/packages/backend/server/src/prelude.ts +++ b/packages/backend/server/src/prelude.ts @@ -1,5 +1,6 @@ import 'reflect-metadata'; +import { cpSync } from 'node:fs'; import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -10,7 +11,22 @@ import { getDefaultAFFiNEConfig, } from './fundamentals/config'; +const configDir = join(fileURLToPath(import.meta.url), '../config'); +async function loadRemote(remoteDir: string, file: string) { + console.log(remoteDir, configDir); + const filePath = join(configDir, file); + if (configDir !== remoteDir) { + console.log('cp remote file'); + cpSync(join(remoteDir, file), filePath, { + force: true, + }); + } + + await import(filePath); +} + async function load() { + const AFFiNE_CONFIG_PATH = process.env.AFFINE_CONFIG_PATH ?? configDir; // Initializing AFFiNE config // // 1. load dotenv file to `process.env` @@ -18,7 +34,7 @@ async function load() { config(); // load `.env` under user config folder config({ - path: join(fileURLToPath(import.meta.url), '../config/.env'), + path: join(AFFiNE_CONFIG_PATH, '.env'), }); // 2. generate AFFiNE default config and assign to `globalThis.AFFiNE` @@ -27,13 +43,13 @@ async function load() { // TODO(@forehalo): // Modules may contribute to ENV_MAP, figure out a good way to involve them instead of hardcoding in `./config/affine.env` // 3. load env => config map to `globalThis.AFFiNE.ENV_MAP - await import('./config/affine.env'); + await loadRemote(AFFiNE_CONFIG_PATH, 'affine.env.js'); // 4. apply `process.env` map overriding to `globalThis.AFFiNE` applyEnvToConfig(globalThis.AFFiNE); - // 5. load `./config/affine` to patch custom configs - await import('./config/affine'); + // 5. load `config/affine` to patch custom configs + await loadRemote(AFFiNE_CONFIG_PATH, 'affine.js'); if (process.env.NODE_ENV === 'development') { console.log('AFFiNE Config:', JSON.stringify(globalThis.AFFiNE, null, 2)); From 2fb0e3ef156edf9fad1dc5ec6ad3d064e40170d5 Mon Sep 17 00:00:00 2001 From: LongYinan Date: Tue, 16 Jan 2024 03:14:57 +0000 Subject: [PATCH 17/92] chore: bump up source-map-loader version to v5 (#5599) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [source-map-loader](https://togithub.com/webpack-contrib/source-map-loader) | [`^4.0.1` -> `^5.0.0`](https://renovatebot.com/diffs/npm/source-map-loader/4.0.1/5.0.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/source-map-loader/5.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/source-map-loader/5.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/source-map-loader/4.0.1/5.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/source-map-loader/4.0.1/5.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
webpack-contrib/source-map-loader (source-map-loader) ### [`v5.0.0`](https://togithub.com/webpack-contrib/source-map-loader/blob/HEAD/CHANGELOG.md#500-2024-01-15) [Compare Source](https://togithub.com/webpack-contrib/source-map-loader/compare/v4.0.2...v5.0.0) ##### ⚠ BREAKING CHANGES - minimum supported Node.js version is `18.12.0` ([#​230](https://togithub.com/webpack-contrib/source-map-loader/issues/230)) ([7fcab17](https://togithub.com/webpack-contrib/source-map-loader/commit/7fcab17805723d5c93eeb0703448b1722cbc93d5)) ##### [4.0.2](https://togithub.com/webpack-contrib/source-map-loader/compare/v4.0.1...v4.0.2) (2023-12-27) ##### Bug Fixes - avoid deprecation message of `abab` package ([#​228](https://togithub.com/webpack-contrib/source-map-loader/issues/228)) ([9daafb3](https://togithub.com/webpack-contrib/source-map-loader/commit/9daafb3662f32c30dc84d068c86085dfad025563)) ##### [4.0.1](https://togithub.com/webpack-contrib/source-map-loader/compare/v4.0.0...v4.0.1) (2022-10-07) ##### Bug Fixes - logic when sourceRoot contains absolute URL ([e724a1f](https://togithub.com/webpack-contrib/source-map-loader/commit/e724a1ffbfd57ec111e406274b76df5ab85d74e1)) ### [`v4.0.2`](https://togithub.com/webpack-contrib/source-map-loader/blob/HEAD/CHANGELOG.md#402-2023-12-27) [Compare Source](https://togithub.com/webpack-contrib/source-map-loader/compare/v4.0.1...v4.0.2)
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/toeverything/AFFiNE). --- packages/frontend/core/package.json | 2 +- yarn.lock | 18 +++++------------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/packages/frontend/core/package.json b/packages/frontend/core/package.json index d4f8bdab23..8b30ab804c 100644 --- a/packages/frontend/core/package.json +++ b/packages/frontend/core/package.json @@ -115,7 +115,7 @@ "lodash-es": "^4.17.21", "mime-types": "^2.1.35", "raw-loader": "^4.0.2", - "source-map-loader": "^4.0.1", + "source-map-loader": "^5.0.0", "style-loader": "^3.3.3", "swc-loader": "^0.2.3", "swc-plugin-coverage-instrument": "^0.0.20", diff --git a/yarn.lock b/yarn.lock index ebdd4845df..3c38fe91fc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -387,7 +387,7 @@ __metadata: react-virtuoso: "npm:^4.6.2" rxjs: "npm:^7.8.1" ses: "npm:^1.0.0" - source-map-loader: "npm:^4.0.1" + source-map-loader: "npm:^5.0.0" style-loader: "npm:^3.3.3" swc-loader: "npm:^0.2.3" swc-plugin-coverage-instrument: "npm:^0.0.20" @@ -14879,13 +14879,6 @@ __metadata: languageName: node linkType: hard -"abab@npm:^2.0.6": - version: 2.0.6 - resolution: "abab@npm:2.0.6" - checksum: ebe95d7278999e605823fc515a3b05d689bc72e7f825536e73c95ebf621636874c6de1b749b3c4bf866b96ccd4b3a2802efa313d0e45ad51a413c8c73247db20 - languageName: node - linkType: hard - "abbrev@npm:1, abbrev@npm:^1.0.0": version: 1.1.1 resolution: "abbrev@npm:1.1.1" @@ -31817,16 +31810,15 @@ __metadata: languageName: node linkType: hard -"source-map-loader@npm:^4.0.1": - version: 4.0.1 - resolution: "source-map-loader@npm:4.0.1" +"source-map-loader@npm:^5.0.0": + version: 5.0.0 + resolution: "source-map-loader@npm:5.0.0" dependencies: - abab: "npm:^2.0.6" iconv-lite: "npm:^0.6.3" source-map-js: "npm:^1.0.2" peerDependencies: webpack: ^5.72.1 - checksum: bdddd7a391bec85a02da47a4ab02d2c63e6736231b712913dd423f1c2a4e8668888813a9213014fcf41fc6991b61c795d703f3a684b7e1c7335bd0a8f9f23ccd + checksum: 9bc90a50df1a3570ddc1ea9cd1aeadb241fd6f6ddb03e72a8f45f5d3fcc357e7edcc9fff8d35d2e338d17edf13c38a7b6e530308ac263d1b462a1e6bfacaf1a1 languageName: node linkType: hard From 75fb0a9f1a3befe779eb290bce8890102516cd72 Mon Sep 17 00:00:00 2001 From: liuyi Date: Tue, 16 Jan 2024 03:27:44 +0000 Subject: [PATCH 18/92] fix(server): standalone early access users detection (#5601) --- .../server/src/modules/features/management.ts | 30 +++++++++++-------- .../server/src/modules/payment/service.ts | 2 +- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/packages/backend/server/src/modules/features/management.ts b/packages/backend/server/src/modules/features/management.ts index 75f11ba1e3..dfccaa8919 100644 --- a/packages/backend/server/src/modules/features/management.ts +++ b/packages/backend/server/src/modules/features/management.ts @@ -47,22 +47,26 @@ export class FeatureManagementService { return this.feature.listFeatureUsers(FeatureType.EarlyAccess); } + async isEarlyAccessUser(email: string) { + const user = await this.prisma.user.findFirst({ + where: { + email, + }, + }); + if (user) { + const canEarlyAccess = await this.feature + .hasUserFeature(user.id, FeatureType.EarlyAccess) + .catch(() => false); + + return canEarlyAccess; + } + return false; + } + /// check early access by email async canEarlyAccess(email: string) { if (this.config.featureFlags.earlyAccessPreview && !this.isStaff(email)) { - const user = await this.prisma.user.findFirst({ - where: { - email, - }, - }); - if (user) { - const canEarlyAccess = await this.feature - .hasUserFeature(user.id, FeatureType.EarlyAccess) - .catch(() => false); - - return canEarlyAccess; - } - return false; + return this.isEarlyAccessUser(email); } else { return true; } diff --git a/packages/backend/server/src/modules/payment/service.ts b/packages/backend/server/src/modules/payment/service.ts index 137c3585bd..8c40c360df 100644 --- a/packages/backend/server/src/modules/payment/service.ts +++ b/packages/backend/server/src/modules/payment/service.ts @@ -679,7 +679,7 @@ export class SubscriptionService { user: User, couponType: CouponType ): Promise { - const earlyAccess = await this.features.canEarlyAccess(user.email); + const earlyAccess = await this.features.isEarlyAccessUser(user.email); if (earlyAccess) { try { const coupon = await this.stripe.coupons.retrieve(couponType); From 94e24d1b82f9d34e841c53c78006f55a143978f4 Mon Sep 17 00:00:00 2001 From: Lewis Liu Date: Mon, 15 Jan 2024 22:39:08 -0800 Subject: [PATCH 19/92] style: import from './index' instead of '.' (#5590) Co-authored-by: LongYinan --- packages/common/workspace/src/list/information.ts | 2 +- packages/common/workspace/src/manager.ts | 2 +- .../components/app-sidebar/add-page-button/index.stories.tsx | 2 +- .../components/app-sidebar/category-divider/index.stories.tsx | 2 +- .../src/components/app-sidebar/menu-item/index.stories.tsx | 2 +- .../components/app-sidebar/quick-search-input/index.stories.tsx | 2 +- .../src/components/app-sidebar/spolight/index.stories.tsx | 2 +- packages/frontend/component/src/ui/divider/divider.stories.tsx | 2 +- packages/frontend/component/src/ui/empty/empty.stories.tsx | 2 +- packages/frontend/component/src/ui/input/input.stories.tsx | 2 +- .../frontend/component/src/ui/menu/menu-trigger.stories.tsx | 2 +- packages/frontend/component/src/ui/menu/menu.stories.tsx | 2 +- .../frontend/component/src/ui/scrollbar/scrollbar.stories.tsx | 2 +- .../frontend/component/src/ui/skeleton/skeleton.stories.tsx | 2 +- packages/frontend/component/src/ui/switch/switch.stories.tsx | 2 +- packages/frontend/component/src/ui/table/table.stories.tsx | 2 +- packages/frontend/component/src/ui/toast/toast.stories.tsx | 2 +- packages/frontend/component/src/ui/tooltip/tooltip.stories.tsx | 2 +- .../cloud/share-header-right-item/authenticated-item.tsx | 2 +- .../src/components/page-list/view/collection-operations.tsx | 2 +- 20 files changed, 20 insertions(+), 20 deletions(-) diff --git a/packages/common/workspace/src/list/information.ts b/packages/common/workspace/src/list/information.ts index 44fae94f27..79b3c4bc76 100644 --- a/packages/common/workspace/src/list/information.ts +++ b/packages/common/workspace/src/list/information.ts @@ -3,7 +3,7 @@ import { Slot } from '@blocksuite/global/utils'; import type { WorkspaceMetadata } from '../metadata'; import type { Workspace } from '../workspace'; -import type { WorkspaceListProvider } from '.'; +import type { WorkspaceListProvider } from './index'; const logger = new DebugLogger('affine:workspace:list:information'); diff --git a/packages/common/workspace/src/manager.ts b/packages/common/workspace/src/manager.ts index 52e4bf98d8..2aa9f3759e 100644 --- a/packages/common/workspace/src/manager.ts +++ b/packages/common/workspace/src/manager.ts @@ -5,8 +5,8 @@ import type { Workspace as BlockSuiteWorkspace } from '@blocksuite/store'; import { fixWorkspaceVersion } from '@toeverything/infra/blocksuite'; import { applyUpdate, encodeStateAsUpdate } from 'yjs'; -import type { BlobStorage } from '.'; import type { WorkspaceFactory } from './factory'; +import type { BlobStorage } from './index'; import type { WorkspaceList } from './list'; import type { WorkspaceMetadata } from './metadata'; import { WorkspacePool } from './pool'; diff --git a/packages/frontend/component/src/components/app-sidebar/add-page-button/index.stories.tsx b/packages/frontend/component/src/components/app-sidebar/add-page-button/index.stories.tsx index 5619d04f58..808e3ac3ae 100644 --- a/packages/frontend/component/src/components/app-sidebar/add-page-button/index.stories.tsx +++ b/packages/frontend/component/src/components/app-sidebar/add-page-button/index.stories.tsx @@ -1,6 +1,6 @@ import type { Meta, StoryFn } from '@storybook/react'; -import { AddPageButton } from '.'; +import { AddPageButton } from './index'; export default { title: 'Components/AppSidebar/AddPageButton', diff --git a/packages/frontend/component/src/components/app-sidebar/category-divider/index.stories.tsx b/packages/frontend/component/src/components/app-sidebar/category-divider/index.stories.tsx index 5239c42c58..7bfb8332e1 100644 --- a/packages/frontend/component/src/components/app-sidebar/category-divider/index.stories.tsx +++ b/packages/frontend/component/src/components/app-sidebar/category-divider/index.stories.tsx @@ -1,6 +1,6 @@ import type { Meta, StoryFn } from '@storybook/react'; -import { CategoryDivider } from '.'; +import { CategoryDivider } from './index'; export default { title: 'Components/AppSidebar/CategoryDivider', diff --git a/packages/frontend/component/src/components/app-sidebar/menu-item/index.stories.tsx b/packages/frontend/component/src/components/app-sidebar/menu-item/index.stories.tsx index 542819a72e..9c3bb7d788 100644 --- a/packages/frontend/component/src/components/app-sidebar/menu-item/index.stories.tsx +++ b/packages/frontend/component/src/components/app-sidebar/menu-item/index.stories.tsx @@ -2,7 +2,7 @@ import { SettingsIcon } from '@blocksuite/icons'; import type { Meta, StoryFn } from '@storybook/react'; import { useState } from 'react'; -import { MenuItem, MenuLinkItem } from '.'; +import { MenuItem, MenuLinkItem } from './index'; export default { title: 'Components/AppSidebar/MenuItem', diff --git a/packages/frontend/component/src/components/app-sidebar/quick-search-input/index.stories.tsx b/packages/frontend/component/src/components/app-sidebar/quick-search-input/index.stories.tsx index 153716b46b..6730a2e602 100644 --- a/packages/frontend/component/src/components/app-sidebar/quick-search-input/index.stories.tsx +++ b/packages/frontend/component/src/components/app-sidebar/quick-search-input/index.stories.tsx @@ -1,6 +1,6 @@ import type { Meta, StoryFn } from '@storybook/react'; -import { QuickSearchInput } from '.'; +import { QuickSearchInput } from './index'; export default { title: 'Components/AppSidebar/QuickSearchInput', diff --git a/packages/frontend/component/src/components/app-sidebar/spolight/index.stories.tsx b/packages/frontend/component/src/components/app-sidebar/spolight/index.stories.tsx index 6952b5c674..e2b3649d91 100644 --- a/packages/frontend/component/src/components/app-sidebar/spolight/index.stories.tsx +++ b/packages/frontend/component/src/components/app-sidebar/spolight/index.stories.tsx @@ -1,7 +1,7 @@ import type { Meta, StoryFn } from '@storybook/react'; import { type PropsWithChildren } from 'react'; -import { Spotlight } from '.'; +import { Spotlight } from './index'; export default { title: 'Components/AppSidebar/Spotlight', diff --git a/packages/frontend/component/src/ui/divider/divider.stories.tsx b/packages/frontend/component/src/ui/divider/divider.stories.tsx index 61bf7db8c3..bdffb91aff 100644 --- a/packages/frontend/component/src/ui/divider/divider.stories.tsx +++ b/packages/frontend/component/src/ui/divider/divider.stories.tsx @@ -1,6 +1,6 @@ import type { Meta, StoryFn } from '@storybook/react'; -import { Divider, type DividerProps } from '.'; +import { Divider, type DividerProps } from './index'; export default { title: 'UI/Divider', diff --git a/packages/frontend/component/src/ui/empty/empty.stories.tsx b/packages/frontend/component/src/ui/empty/empty.stories.tsx index 3ef2e3f893..15ba1c497d 100644 --- a/packages/frontend/component/src/ui/empty/empty.stories.tsx +++ b/packages/frontend/component/src/ui/empty/empty.stories.tsx @@ -1,6 +1,6 @@ import type { Meta, StoryFn } from '@storybook/react'; -import { Empty, type EmptyContentProps } from '.'; +import { Empty, type EmptyContentProps } from './index'; export default { title: 'UI/Empty', diff --git a/packages/frontend/component/src/ui/input/input.stories.tsx b/packages/frontend/component/src/ui/input/input.stories.tsx index 1c7572a596..2c7819532d 100644 --- a/packages/frontend/component/src/ui/input/input.stories.tsx +++ b/packages/frontend/component/src/ui/input/input.stories.tsx @@ -1,7 +1,7 @@ import { InformationIcon } from '@blocksuite/icons'; import type { Meta, StoryFn } from '@storybook/react'; -import { Input, type InputProps } from '.'; +import { Input, type InputProps } from './index'; export default { title: 'UI/Input', diff --git a/packages/frontend/component/src/ui/menu/menu-trigger.stories.tsx b/packages/frontend/component/src/ui/menu/menu-trigger.stories.tsx index c6222af142..6a888057a4 100644 --- a/packages/frontend/component/src/ui/menu/menu-trigger.stories.tsx +++ b/packages/frontend/component/src/ui/menu/menu-trigger.stories.tsx @@ -1,6 +1,6 @@ import type { Meta, StoryFn } from '@storybook/react'; -import { MenuTrigger, type MenuTriggerProps } from '.'; +import { MenuTrigger, type MenuTriggerProps } from './index'; export default { title: 'UI/MenuTrigger', diff --git a/packages/frontend/component/src/ui/menu/menu.stories.tsx b/packages/frontend/component/src/ui/menu/menu.stories.tsx index 6509f3e7ba..849128bc8c 100644 --- a/packages/frontend/component/src/ui/menu/menu.stories.tsx +++ b/packages/frontend/component/src/ui/menu/menu.stories.tsx @@ -13,7 +13,7 @@ import { MenuSeparator, MenuSub, MenuTrigger, -} from '.'; +} from './index'; export default { title: 'UI/Menu', diff --git a/packages/frontend/component/src/ui/scrollbar/scrollbar.stories.tsx b/packages/frontend/component/src/ui/scrollbar/scrollbar.stories.tsx index ff8bd3b3bc..a7f470f3f7 100644 --- a/packages/frontend/component/src/ui/scrollbar/scrollbar.stories.tsx +++ b/packages/frontend/component/src/ui/scrollbar/scrollbar.stories.tsx @@ -1,6 +1,6 @@ import type { Meta, StoryFn } from '@storybook/react'; -import { ScrollableContainer, type ScrollableContainerProps } from '.'; +import { ScrollableContainer, type ScrollableContainerProps } from './index'; export default { title: 'UI/Scrollbar', diff --git a/packages/frontend/component/src/ui/skeleton/skeleton.stories.tsx b/packages/frontend/component/src/ui/skeleton/skeleton.stories.tsx index 58b5b94b0f..8ee1f7c3d3 100644 --- a/packages/frontend/component/src/ui/skeleton/skeleton.stories.tsx +++ b/packages/frontend/component/src/ui/skeleton/skeleton.stories.tsx @@ -1,6 +1,6 @@ import type { Meta, StoryFn } from '@storybook/react'; -import { Skeleton, type SkeletonProps } from '.'; +import { Skeleton, type SkeletonProps } from './index'; export default { title: 'UI/Skeleton', diff --git a/packages/frontend/component/src/ui/switch/switch.stories.tsx b/packages/frontend/component/src/ui/switch/switch.stories.tsx index f50875ac78..c9d08eedd1 100644 --- a/packages/frontend/component/src/ui/switch/switch.stories.tsx +++ b/packages/frontend/component/src/ui/switch/switch.stories.tsx @@ -1,6 +1,6 @@ import type { Meta, StoryFn } from '@storybook/react'; -import { Switch, type SwitchProps } from '.'; +import { Switch, type SwitchProps } from './index'; export default { title: 'UI/Switch', diff --git a/packages/frontend/component/src/ui/table/table.stories.tsx b/packages/frontend/component/src/ui/table/table.stories.tsx index 2d48a07b16..14f80f3f42 100644 --- a/packages/frontend/component/src/ui/table/table.stories.tsx +++ b/packages/frontend/component/src/ui/table/table.stories.tsx @@ -7,7 +7,7 @@ import { TableCell, TableHead, TableHeadRow, -} from '.'; +} from './index'; export default { title: 'UI/Table', diff --git a/packages/frontend/component/src/ui/toast/toast.stories.tsx b/packages/frontend/component/src/ui/toast/toast.stories.tsx index 0a058f473c..bf29afbc15 100644 --- a/packages/frontend/component/src/ui/toast/toast.stories.tsx +++ b/packages/frontend/component/src/ui/toast/toast.stories.tsx @@ -1,7 +1,7 @@ import { useCallback, useState } from 'react'; import { Button } from '../button'; -import { toast } from '.'; +import { toast } from './index'; export default { title: 'UI/Toast', diff --git a/packages/frontend/component/src/ui/tooltip/tooltip.stories.tsx b/packages/frontend/component/src/ui/tooltip/tooltip.stories.tsx index fcba72f979..13942d6628 100644 --- a/packages/frontend/component/src/ui/tooltip/tooltip.stories.tsx +++ b/packages/frontend/component/src/ui/tooltip/tooltip.stories.tsx @@ -1,7 +1,7 @@ import type { Meta, StoryFn } from '@storybook/react'; import { Button } from '../button'; -import Tooltip, { type TooltipProps } from '.'; +import Tooltip, { type TooltipProps } from './index'; export default { title: 'UI/Tooltip', diff --git a/packages/frontend/core/src/components/cloud/share-header-right-item/authenticated-item.tsx b/packages/frontend/core/src/components/cloud/share-header-right-item/authenticated-item.tsx index a12fd69838..0ec6d87d83 100644 --- a/packages/frontend/core/src/components/cloud/share-header-right-item/authenticated-item.tsx +++ b/packages/frontend/core/src/components/cloud/share-header-right-item/authenticated-item.tsx @@ -4,7 +4,7 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useCurrentUser } from '../../../hooks/affine/use-current-user'; import { useMembers } from '../../../hooks/affine/use-members'; import { useNavigateHelper } from '../../../hooks/use-navigate-helper'; -import type { ShareHeaderRightItemProps } from '.'; +import type { ShareHeaderRightItemProps } from './index'; export const AuthenticatedItem = ({ ...props }: ShareHeaderRightItemProps) => { const { workspaceId, pageId } = props; diff --git a/packages/frontend/core/src/components/page-list/view/collection-operations.tsx b/packages/frontend/core/src/components/page-list/view/collection-operations.tsx index a8829cb5cf..a4889d6ec5 100644 --- a/packages/frontend/core/src/components/page-list/view/collection-operations.tsx +++ b/packages/frontend/core/src/components/page-list/view/collection-operations.tsx @@ -15,8 +15,8 @@ import { } from 'react'; import type { useCollectionManager } from '../use-collection-manager'; -import type { AllPageListConfig } from '.'; import * as styles from './collection-operations.css'; +import type { AllPageListConfig } from './index'; import { useEditCollection, useEditCollectionName, From dea0aab5e31836bb40005b570fbc59f12b4ccc25 Mon Sep 17 00:00:00 2001 From: DarkSky Date: Tue, 16 Jan 2024 06:41:45 +0000 Subject: [PATCH 20/92] feat: update nx config (#5597) --- nx.json | 5 +++-- package.json | 3 +-- yarn.lock | 5 ++--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/nx.json b/nx.json index 69c3b1f5ef..be8c264e7f 100644 --- a/nx.json +++ b/nx.json @@ -1,12 +1,13 @@ { "$schema": "./node_modules/nx/schemas/nx-schema.json", "npmScope": "toeverything", + "nxCloudAccessToken": "MzUwNTU4YWItZGFhYi00YjE2LWIxODAtODk4NmIwYjMwYzZkfHJlYWQ=", "tasksRunnerOptions": { "default": { - "runner": "nx-cloud", + "runner": "nx/tasks-runners/default", "options": { "cacheableOperations": ["build", "test", "e2e", "lint"], - "accessToken": "YmQ2NTg1ODktZTk5Mi00YzhiLTk2ZmUtNWQzMDg0NDBkOWM3fHJlYWQtb25seQ==" + "runtimeCacheInputs": ["node -v"] } } }, diff --git a/package.json b/package.json index dd8bbf264b..46934e073c 100644 --- a/package.json +++ b/package.json @@ -93,8 +93,7 @@ "lint-staged": "^15.1.0", "msw": "^2.0.8", "nanoid": "^5.0.3", - "nx": "^17.1.3", - "nx-cloud": "^16.5.2", + "nx": "^17.2.8", "nyc": "^15.1.0", "oxlint": "0.0.22", "prettier": "^3.1.0", diff --git a/yarn.lock b/yarn.lock index 3c38fe91fc..d995c6d8ea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -583,8 +583,7 @@ __metadata: lint-staged: "npm:^15.1.0" msw: "npm:^2.0.8" nanoid: "npm:^5.0.3" - nx: "npm:^17.1.3" - nx-cloud: "npm:^16.5.2" + nx: "npm:^17.2.8" nyc: "npm:^15.1.0" oxlint: "npm:0.0.22" prettier: "npm:^3.1.0" @@ -27749,7 +27748,7 @@ __metadata: languageName: node linkType: hard -"nx@npm:17.2.8, nx@npm:^17.1.3": +"nx@npm:17.2.8, nx@npm:^17.1.3, nx@npm:^17.2.8": version: 17.2.8 resolution: "nx@npm:17.2.8" dependencies: From d6dd837bdb239f1c869814f52cd3e10e9066e768 Mon Sep 17 00:00:00 2001 From: Joooye_34 Date: Tue, 16 Jan 2024 07:42:32 +0000 Subject: [PATCH 21/92] chore: update favicon (#5566) --- packages/frontend/core/public/favicon-144.png | Bin 4473 -> 11119 bytes packages/frontend/core/public/favicon-192.png | Bin 6133 -> 15885 bytes packages/frontend/core/public/favicon-32.png | Bin 0 -> 1353 bytes packages/frontend/core/public/favicon-36.png | Bin 1061 -> 1550 bytes packages/frontend/core/public/favicon-48.png | Bin 1449 -> 2187 bytes packages/frontend/core/public/favicon-72.png | Bin 2208 -> 4148 bytes packages/frontend/core/public/favicon-96.png | Bin 3053 -> 6248 bytes 7 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 packages/frontend/core/public/favicon-32.png diff --git a/packages/frontend/core/public/favicon-144.png b/packages/frontend/core/public/favicon-144.png index 29ef373eeddf7cc1b52ba29809da5925a201f97f..f117fa89f031d596ff8f992ab8477dcccd194e7b 100644 GIT binary patch literal 11119 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 BT;Tu! delta 4429 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 delta 6102 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 delta 990 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 D=Y$*S delta 1381 zcmV-r1)BPc5vdE1ReuFONkl2cL8W<* zhKhF!Dk>_98!8oEqayJ_Me*)a(W!hSKIRFf5F);Uw}1OuC+AM)a%SetHU7ys`|PvN z%>TdE+H37|wt#p*TU%Sm976IFv9z=l_A)LIt|F0}S~u#^R+%M@K0~9>8gT ze_t>#QKJ1;K!0m%>mTOOc6@v+sScvU2xw%Ua;_fuba8Q!l9rDI9N|im;oJ%^v%K{3 z@=`tuNG>lK@|yso=GXViH1Pld@{-=2N$hv{6c-oE*4CD+udho@O^x^oH8uq{Ncxl{ zV19ny=5K@y4-ZR{5DH2X(9zLhiJzRD*iT(uo$X5ol7Av#U|_%=r>3T4XlTf`M@B}h zNBoBG0w9m|S65eMVPQcwHa6@B^4;Ct;x~L10C6Qr=qSd<#_W-6etW@J0sZ~`4#A$D z9_j4twC(NfZJC{&wb%Tf(7p-4`bv0hZOtCX$H$$%$;nBfSaQy!_6xoVV3I0&dwZR* zQ4x^G3x5Dl`2}ADkRqF!nrxfc#~U*^3GBY!6UtwXPC6=3;D~pSZ za&T}U4Gj%;j%X@14^VP7t?{+vZ^#xC!b)&df1qX^pti(NU+UDdP1)%mPT8 z%S-gT6?m-CT=>wVoepFXp z!!KS3x+^DZ98g^dzpZsxw$!8+(-a!(h$X4j6o!#xR)&En*jG(-~Gi(MAtR|0)P4% z1#(=wy1KIQjX3RtG@;`TKh7}2g@fLkI#KHLJL-OFvfCdI4$1|0p~( zvmpS%%#*heczLaUw70h>4-XGf2OSaVaczl4mj$b zF@LOpm6esR#w}m<;Uty8Ua`TF?%F%cX(OOJCnqOdQBjeSo`6$XS(%u*%3enIXYG&n zuLfUevXx0eVL$4>X{C$?YN!Ibw!7~;l|e-Y}D n7Xx0uC0Uz+ui!#!)NA-3W4PmeL4sOM00000NkvXXu0mjf#~`9J diff --git a/packages/frontend/core/public/favicon-72.png b/packages/frontend/core/public/favicon-72.png index c8668f88c51d6fd8f92a8a18bc9d64e8db3c7d5d..326ada11b9c17e8f1df4aefcdcf78bf944e6637b 100644 GIT binary patch delta 4101 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 D;W7eW delta 2146 zcmV-o2%Yz|AfOSDReuOINkl>UPi|O+Vaw~OeN+=k4H`A-SKoCiwFN$y9zIDwE z$iZoQw3RsdN|0|TQjU#{xn{@v_wPf=tbsBRR! z>gnm}35R?S?HqX`SAwiU`uh4puB~_2uPZ@u;)gm=5F{}{1t^7inMi`XJi~t72qIL+-#!WgM6H7d4<1Nwh(b96GP- z74|bHs8U!58ZgfH&Ye4>({S` zYpz_mk}R5OK`jpZ^-xi!1WlzLuzmaXK!hbqW_R)8#Xt@usUDK9UcH)J4?$V48D&aP zT1d8YE=U*}8VV)P{|WaZ3i2?!?(Xg~@*rG_Lq%DK{Z^L|;-pXmgdq72okIo(Ab;;+ z@=877oOv2;C}-QYZDmO$;61KgyH>W)R!d~2AXFhR@f0cd?%hl3A)}+CBPfRuuVhh^ zSDBBL!#&c7$Wld_CTq=vOUw?CgY-C}OM5!%XwuIM}zF|x(K9mP>4 zB#U;9IySv{Id`_G89_GGVghoq@;}zRfXKp>7^Hlch*lj$KsNUw;>lcTm%q~FR;cA{ zzb0WmegJ4^<96Z80)iMM?f4axVMD%%AV1o@dv}sZaviITm~Z@kik`I$`!!Ke+5C0?>XEZAlvk^l{p+J2l|05{OtG@-Ns`VhAbbm0j$4^0$C5LuY za^%R7Ft6hpySmK0i8pO8ineur=!&(1L?224Aoq$!0yC=pWCA{; zebzryR80_4*N107Rz1KH^zh-sN!LS+B}Zz~f|&*N5W-O@Nwm<)qtwP{wa?dPf~p9L zlS1p51?xN+6xYbHrhmbv1Q37&mr|JLJ|do}Tc5(( z&~&p~$WU~PRW=jErir5KMo=vAZ#~GHN$Ou|&1beK{ephRs49xtsl;4B9ARgIsue^= zONP5*#R~VmLrb~UU{;AD30$Ki_n!fOZ_porg))Np*Il!t^nXw2|6iBNVKV8pXFWE-0qOP4MUCHs3&27=h=qfjV}l>T7vnit}CO-)Qp^g#|=H8d+2I~;iPx~;l|pw`yb^QeeU&FHKdGkbZ!0XxE|KCvgi zW;3>N=YqoIQ(Gw^X7S?1Zq=$)A@9TI_%WRF z%9;KKOG0T!n3Mh{KYOBzu7LnCL~eqUc6v$e=cb9$CT~UU(lCcNc^f7%p6(`Z^Y{k- Y15=c}NrkGG&Tu)259p1eb%8l+7W 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 Date: Tue, 16 Jan 2024 07:57:23 +0000 Subject: [PATCH 22/92] feat(core): get cloud workspace usage limit from user quota (#5518) --- .../src/components/setting-components/storage-progess.tsx | 6 +++++- .../affine/setting-modal/account-setting/index.tsx | 8 ++++++-- packages/frontend/i18n/src/resources/en.json | 1 + 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/frontend/component/src/components/setting-components/storage-progess.tsx b/packages/frontend/component/src/components/setting-components/storage-progess.tsx index 174b6c83d8..e5cb1c7053 100644 --- a/packages/frontend/component/src/components/setting-components/storage-progess.tsx +++ b/packages/frontend/component/src/components/setting-components/storage-progess.tsx @@ -68,7 +68,11 @@ export const StorageProgress = ({ {upgradable ? ( - - - ); - }; - const renderCustomMonthHeader = ({ - date, - decreaseYear, - increaseYear, - prevYearButtonDisabled, - nextYearButtonDisabled, - }: { - date: Date; - decreaseYear: () => void; - increaseYear: () => void; - prevYearButtonDisabled: boolean; - nextYearButtonDisabled: boolean; - }) => { - const selectedYear = dayjs(date).year(); - return ( -
-
- {selectedYear} -
- - -
- ); - }; - return ( - styles.weekStyle} - dayClassName={() => styles.dayStyle} - popperClassName={styles.popperStyle} - monthClassName={() => styles.mouthsStyle} - selected={selectedDate} - onChange={handleSelectDate} - showPopperArrow={false} - dateFormat="MMM dd" - showMonthYearPicker={openMonthPicker} - shouldCloseOnSelect={!openMonthPicker} - renderCustomHeader={({ - date, - decreaseYear, - increaseYear, - decreaseMonth, - increaseMonth, - prevYearButtonDisabled, - nextYearButtonDisabled, - prevMonthButtonDisabled, - nextMonthButtonDisabled, - }) => - openMonthPicker - ? renderCustomMonthHeader({ - date, - decreaseYear, - increaseYear, - prevYearButtonDisabled, - nextYearButtonDisabled, - }) - : renderCustomHeader({ - date, - decreaseMonth, - increaseMonth, - prevMonthButtonDisabled, - nextMonthButtonDisabled, - }) - } - /> - ); -}; - -export default AFFiNEDatePicker; diff --git a/packages/frontend/component/src/index.ts b/packages/frontend/component/src/index.ts index aed6a07316..d325cdb793 100644 --- a/packages/frontend/component/src/index.ts +++ b/packages/frontend/component/src/index.ts @@ -3,6 +3,7 @@ export * from './styles'; export * from './ui/avatar'; export * from './ui/button'; export * from './ui/checkbox'; +export * from './ui/date-picker'; export * from './ui/divider'; export * from './ui/empty'; export * from './ui/input'; diff --git a/packages/frontend/component/src/theme/global.css b/packages/frontend/component/src/theme/global.css index 8f51d01a76..66a35fc6ee 100644 --- a/packages/frontend/component/src/theme/global.css +++ b/packages/frontend/component/src/theme/global.css @@ -1,4 +1,3 @@ -@import 'react-datepicker/dist/react-datepicker.css'; @import './fonts.css'; * { diff --git a/packages/frontend/component/src/ui/date-picker/blocksuite-date-picker.stories.tsx b/packages/frontend/component/src/ui/date-picker/blocksuite-date-picker.stories.tsx new file mode 100644 index 0000000000..b18a90a7c4 --- /dev/null +++ b/packages/frontend/component/src/ui/date-picker/blocksuite-date-picker.stories.tsx @@ -0,0 +1,15 @@ +import type { Meta, StoryFn } from '@storybook/react'; + +import { BlocksuiteDatePicker } from './blocksuite-date-picker'; + +export default { + title: 'UI/Date Picker/Blocksuite Date Picker', +} satisfies Meta; + +export const Basic: StoryFn = () => { + return ( +
+ +
+ ); +}; diff --git a/packages/frontend/component/src/ui/date-picker/blocksuite-date-picker.tsx b/packages/frontend/component/src/ui/date-picker/blocksuite-date-picker.tsx new file mode 100644 index 0000000000..fa97bc93c1 --- /dev/null +++ b/packages/frontend/component/src/ui/date-picker/blocksuite-date-picker.tsx @@ -0,0 +1,11 @@ +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +import { DatePicker } from '@blocksuite/blocks/src/_common/components/date-picker/index'; +import { createComponent } from '@lit/react'; +import React from 'react'; + +export const BlocksuiteDatePicker = createComponent({ + tagName: 'date-picker', + elementClass: DatePicker, + react: React, + events: {}, +}); diff --git a/packages/frontend/component/src/ui/date-picker/date-picker.stories.tsx b/packages/frontend/component/src/ui/date-picker/date-picker.stories.tsx new file mode 100644 index 0000000000..dd652b5ea4 --- /dev/null +++ b/packages/frontend/component/src/ui/date-picker/date-picker.stories.tsx @@ -0,0 +1,37 @@ +import type { Meta, StoryFn } from '@storybook/react'; +import dayjs from 'dayjs'; +import { useState } from 'react'; + +import { AFFiNEDatePicker } from '.'; + +export default { + title: 'UI/Date Picker/Date Picker', +} satisfies Meta; + +const _format = 'YYYY-MM-DD'; + +const Template: StoryFn = args => { + const [date, setDate] = useState(dayjs().format(_format)); + return ( +
+
Selected Date: {date}
+ + { + setDate(dayjs(e, _format).format(_format)); + }} + /> +
+ ); +}; + +export const Basic: StoryFn = Template.bind(undefined); +Basic.args = {}; + +export const Inline: StoryFn = + Template.bind(undefined); +Inline.args = { + inline: true, +}; diff --git a/packages/frontend/component/src/ui/date-picker/date-picker.tsx b/packages/frontend/component/src/ui/date-picker/date-picker.tsx new file mode 100644 index 0000000000..99cb499e88 --- /dev/null +++ b/packages/frontend/component/src/ui/date-picker/date-picker.tsx @@ -0,0 +1,257 @@ +import { + ArrowDownSmallIcon, + ArrowLeftSmallIcon, + ArrowRightSmallIcon, +} from '@blocksuite/icons'; +import clsx from 'clsx'; +import dayjs from 'dayjs'; +import { type HTMLAttributes, useCallback, useState } from 'react'; +import DatePicker, { type ReactDatePickerProps } from 'react-datepicker'; + +import * as styles from './index.css'; +const months = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', +]; +export interface AFFiNEDatePickerProps + extends Omit { + value?: string; + onChange: (value: string) => void; +} + +interface HeaderLayoutProps extends HTMLAttributes { + length: number; + left: React.ReactNode; + right: React.ReactNode; +} + +/** + * The `DatePicker` should work with different width + * This is a hack to make header's item align with calendar cell's label, **instead of the cell** + * @param length: number of items that calendar body row has + */ +const HeaderLayout = ({ + length, + left, + right, + className, + ...attrs +}: HeaderLayoutProps) => { + return ( +
+ {Array.from({ length }) + .fill(0) + .map((_, index) => { + const isLeft = index === 0; + const isRight = index === length - 1; + return ( +
+
+ {isLeft ? left : isRight ? right : null} +
+
+ ); + })} +
+ ); +}; + +export const AFFiNEDatePicker = ({ + value, + onChange, + ...props +}: AFFiNEDatePickerProps) => { + const [openMonthPicker, setOpenMonthPicker] = useState(false); + const [selectedDate, setSelectedDate] = useState( + value ? dayjs(value).toDate() : null + ); + const handleOpenMonthPicker = useCallback(() => { + setOpenMonthPicker(true); + }, []); + const handleCloseMonthPicker = useCallback(() => { + setOpenMonthPicker(false); + }, []); + const handleSelectDate = (date: Date | null) => { + if (date) { + setSelectedDate(date); + onChange(dayjs(date).format('YYYY-MM-DD')); + setOpenMonthPicker(false); + } + }; + const renderCustomHeader = ({ + date, + decreaseMonth, + increaseMonth, + prevMonthButtonDisabled, + nextMonthButtonDisabled, + }: { + date: Date; + decreaseMonth: () => void; + increaseMonth: () => void; + prevMonthButtonDisabled: boolean; + nextMonthButtonDisabled: boolean; + }) => { + const selectedYear = dayjs(date).year(); + const selectedMonth = dayjs(date).month(); + return ( + +
+ {months[selectedMonth]} +
+
+ {selectedYear} +
+
+ +
+ + } + right={ +
+ + +
+ } + /> + ); + }; + const renderCustomMonthHeader = ({ + date, + decreaseYear, + increaseYear, + prevYearButtonDisabled, + nextYearButtonDisabled, + }: { + date: Date; + decreaseYear: () => void; + increaseYear: () => void; + prevYearButtonDisabled: boolean; + nextYearButtonDisabled: boolean; + }) => { + const selectedYear = dayjs(date).year(); + return ( + + {selectedYear} + + } + right={ +
+ + +
+ } + /> + ); + }; + return ( + styles.weekStyle} + dayClassName={() => styles.dayStyle} + popperClassName={styles.popperStyle} + monthClassName={() => styles.mouthsStyle} + selected={selectedDate} + onChange={handleSelectDate} + showPopperArrow={false} + dateFormat="MMM dd" + showMonthYearPicker={openMonthPicker} + shouldCloseOnSelect={!openMonthPicker} + renderCustomHeader={({ + date, + decreaseYear, + increaseYear, + decreaseMonth, + increaseMonth, + prevYearButtonDisabled, + nextYearButtonDisabled, + prevMonthButtonDisabled, + nextMonthButtonDisabled, + }) => + openMonthPicker + ? renderCustomMonthHeader({ + date, + decreaseYear, + increaseYear, + prevYearButtonDisabled, + nextYearButtonDisabled, + }) + : renderCustomHeader({ + date, + decreaseMonth, + increaseMonth, + prevMonthButtonDisabled, + nextMonthButtonDisabled, + }) + } + {...props} + /> + ); +}; + +export default AFFiNEDatePicker; diff --git a/packages/frontend/component/src/components/date-picker/index.css.ts b/packages/frontend/component/src/ui/date-picker/index.css.ts similarity index 51% rename from packages/frontend/component/src/components/date-picker/index.css.ts rename to packages/frontend/component/src/ui/date-picker/index.css.ts index ddbccbed47..f7b7757085 100644 --- a/packages/frontend/component/src/components/date-picker/index.css.ts +++ b/packages/frontend/component/src/ui/date-picker/index.css.ts @@ -1,5 +1,38 @@ import { globalStyle, style } from '@vanilla-extract/css'; +/** + * we do not import css from 'react-date-picker' anymore + **/ +globalStyle('.react-datepicker__aria-live', { + display: 'none', +}); + +export const basicCell = style({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + minWidth: '28px', + maxWidth: '50px', + flex: '1', + userSelect: 'none', +}); +export const headerLayoutCell = style([basicCell]); +export const headerLayoutCellOrigin = style({ + width: 0, + height: 'fit-content', + display: 'flex', + selectors: { + '[data-is-left="true"] &': { + justifyContent: 'flex-start', + marginLeft: '-12px', + }, + '[data-is-right="true"] &': { + justifyContent: 'flex-end', + marginRight: '-24px', + }, + }, +}); + export const inputStyle = style({ fontSize: 'var(--affine-font-xs)', width: '50px', @@ -16,52 +49,40 @@ export const inputStyle = style({ }); export const popperStyle = style({ boxShadow: 'var(--affine-shadow-2)', - padding: '0 10px', + // TODO: for menu offset, need to be optimized marginTop: '16px', background: 'var(--affine-background-overlay-panel-color)', borderRadius: '12px', width: '300px', + zIndex: 'var(--affine-z-index-popover)', }); globalStyle('.react-datepicker__header', { - background: 'var(--affine-background-overlay-panel-color)', + background: 'none', border: 'none', - marginBottom: '6px', + marginBottom: '8px', +}); +globalStyle('.react-datepicker__header, .react-datepicker__month', { + display: 'flex', + flexDirection: 'column', + gap: '8px', }); export const headerStyle = style({ - background: 'var(--affine-background-overlay-panel-color)', border: 'none', display: 'flex', - width: '100%', alignItems: 'center', - marginBottom: '12px', - padding: '0 14px', - position: 'relative', + justifyContent: 'space-between', + width: '100%', }); export const monthHeaderStyle = style({ - background: 'var(--affine-background-overlay-panel-color)', border: 'none', - display: 'flex', width: '100%', - alignItems: 'center', marginBottom: '18px', - padding: '0 14px', - position: 'relative', - '::after': { - content: '""', - position: 'absolute', - width: 'calc(100% - 24px)', - height: '1px', - background: 'var(--affine-border-color)', - bottom: '-18px', - left: '12px', - }, }); export const monthTitleStyle = style({ color: 'var(--affine-text-primary-color)', fontWeight: '600', fontSize: 'var(--affine-font-sm)', - marginLeft: '12px', }); export const yearStyle = style({ marginLeft: '8px', @@ -73,77 +94,92 @@ export const mouthStyle = style({ color: 'var(--affine-text-primary-color)', fontWeight: '600', fontSize: 'var(--affine-font-sm)', + cursor: 'pointer', + textAlign: 'center', }); -export const arrowLeftStyle = style({ +export const headerLabel = style({ + display: 'flex', + alignItems: 'center', +}); +export const headerActionWrapper = style({ + display: 'flex', + alignItems: 'center', + gap: 24, +}); +export const headerAction = style({ width: '16px', height: '16px', - textAlign: 'right', - position: 'absolute', - right: '50px', }); -export const arrowRightStyle = style({ - width: '16px', - height: '16px', - right: '14px', - position: 'absolute', + +globalStyle('.react-datepicker__day-names, .react-datepicker__week', { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + gap: 8, }); -export const weekStyle = style({ - fontSize: 'var(--affine-font-xs)', - color: 'var(--affine-text-secondary-color)', - display: 'inline-block', - width: '28px', - height: '28px', - lineHeight: '28px', - padding: '0 4px', - margin: '0px 6px', - verticalAlign: 'middle', +export const row = style({ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + gap: 8, }); +// header day cell +export const weekStyle = style([ + basicCell, + { + height: '28px', + fontSize: 'var(--affine-font-xs)', + fontWeight: 500, + color: 'var(--affine-text-secondary-color)', + }, +]); export const calendarStyle = style({ - background: 'var(--affine-background-overlay-panel-color)', + background: 'none', border: 'none', width: '100%', + padding: '20px', }); -export const dayStyle = style({ - fontSize: 'var(--affine-font-xs)', - color: 'var(--affine-text-primary-color)', - display: 'inline-block', - width: '28px', - height: '28px', - lineHeight: '28px', - padding: '0 4px', - margin: '6px 12px 6px 0px', - verticalAlign: 'middle', - fontWeight: '400', - borderRadius: '8px', - selectors: { - '&:hover': { - background: 'var(--affine-hover-color)', - borderRadius: '8px', - transition: 'background-color 0.3s ease-in-out', - }, - '&[aria-selected="true"]': { - color: 'var(--affine-black)', - background: 'var(--affine-hover-color)', - }, - '&[aria-selected="true"]:hover': { - background: 'var(--affine-hover-color)', - }, - '&[tabindex="0"][aria-selected="false"]': { - background: 'var(--affine-background-overlay-panel-color)', - }, - '&.react-datepicker__day--today[aria-selected="false"]': { - background: 'var(--affine-primary-color)', - color: 'var(--affine-palette-line-white)', - }, - '&.react-datepicker__day--today[aria-selected="false"]:hover': { - color: 'var(--affine-black)', - background: 'var(--affine-hover-color)', - }, - '&.react-datepicker__day--outside-month[aria-selected="false"]': { - color: 'var(--affine-text-disable-color)', + +export const dayStyle = style([ + basicCell, + { + height: '28px', + fontSize: 'var(--affine-font-xs)', + color: 'var(--affine-text-primary-color)', + cursor: 'pointer', + + fontWeight: '400', + borderRadius: '8px', + selectors: { + '&:hover': { + background: 'var(--affine-hover-color)', + borderRadius: '8px', + transition: 'background-color 0.3s ease-in-out', + }, + '&[aria-selected="true"]': { + color: 'var(--affine-black)', + background: 'var(--affine-hover-color)', + }, + '&[aria-selected="true"]:hover': { + background: 'var(--affine-hover-color)', + }, + '&[tabindex="0"][aria-selected="false"]': { + background: 'var(--affine-background-overlay-panel-color)', + }, + '&.react-datepicker__day--today[aria-selected="false"]': { + background: 'var(--affine-primary-color)', + color: 'var(--affine-palette-line-white)', + }, + '&.react-datepicker__day--today[aria-selected="false"]:hover': { + color: 'var(--affine-black)', + background: 'var(--affine-hover-color)', + }, + '&.react-datepicker__day--outside-month[aria-selected="false"]': { + color: 'var(--affine-text-disable-color)', + }, }, }, -}); +]); export const arrowDownStyle = style({ width: '16px', height: '16px', diff --git a/packages/frontend/component/src/components/date-picker/index.tsx b/packages/frontend/component/src/ui/date-picker/index.ts similarity index 100% rename from packages/frontend/component/src/components/date-picker/index.tsx rename to packages/frontend/component/src/ui/date-picker/index.ts diff --git a/packages/frontend/core/src/components/page-list/filter/literal-matcher.tsx b/packages/frontend/core/src/components/page-list/filter/literal-matcher.tsx index 83cacf6f8d..fc3e259fec 100644 --- a/packages/frontend/core/src/components/page-list/filter/literal-matcher.tsx +++ b/packages/frontend/core/src/components/page-list/filter/literal-matcher.tsx @@ -1,5 +1,4 @@ -import { Input, Menu, MenuItem } from '@affine/component'; -import { AFFiNEDatePicker } from '@affine/component/date-picker'; +import { AFFiNEDatePicker, Input, Menu, MenuItem } from '@affine/component'; import type { LiteralValue, Tag } from '@affine/env/filter'; import dayjs from 'dayjs'; import { type ReactNode } from 'react'; diff --git a/tests/storybook/src/stories/datepicker.stories.tsx b/tests/storybook/src/stories/datepicker.stories.tsx index f69cc96359..ccfcb532b3 100644 --- a/tests/storybook/src/stories/datepicker.stories.tsx +++ b/tests/storybook/src/stories/datepicker.stories.tsx @@ -1,4 +1,4 @@ -import { AFFiNEDatePicker } from '@affine/component/date-picker'; +import { AFFiNEDatePicker } from '@affine/component'; import type { Meta, StoryFn } from '@storybook/react'; import { useState } from 'react'; @@ -8,7 +8,7 @@ export default { parameters: { chromatic: { disableSnapshot: true }, }, -} satisfies Meta; +} satisfies Meta; export const Default: StoryFn = () => { const [value, setValue] = useState(new Date().toString()); diff --git a/yarn.lock b/yarn.lock index 2eb4fc0dad..1f9e6e3211 100644 --- a/yarn.lock +++ b/yarn.lock @@ -219,6 +219,7 @@ __metadata: "@emotion/react": "npm:^11.11.1" "@emotion/server": "npm:^11.11.0" "@emotion/styled": "npm:^11.11.0" + "@lit/react": "npm:^1.0.2" "@popperjs/core": "npm:^2.11.8" "@radix-ui/react-avatar": "npm:^1.0.4" "@radix-ui/react-collapsible": "npm:^1.0.3" @@ -6488,6 +6489,15 @@ __metadata: languageName: node linkType: hard +"@lit/react@npm:^1.0.2": + version: 1.0.2 + resolution: "@lit/react@npm:1.0.2" + peerDependencies: + "@types/react": 17 || 18 + checksum: 78bc607f9022ceefa5c714a701b71e444df8d6c88c75b0fe728b7157d3ec00a7fa459bc3c4ce5a4b5315079159c9759be4738e5ade656f8d74e32f729f75efc4 + languageName: node + linkType: hard + "@lit/reactive-element@npm:^2.0.0": version: 2.0.2 resolution: "@lit/reactive-element@npm:2.0.2" From 00acc49342ba037ca3e301ea1fa8b1890d3ec0db Mon Sep 17 00:00:00 2001 From: liuyi Date: Wed, 17 Jan 2024 10:22:35 +0000 Subject: [PATCH 32/92] chore(server): remove octobase storage usage (#5594) since all blobs have been successfully migrated to r2, the octobase blob functions are no longer necessary. --- .../src/modules/storage/wrappers/blob.ts | 48 ++----------------- 1 file changed, 4 insertions(+), 44 deletions(-) diff --git a/packages/backend/server/src/modules/storage/wrappers/blob.ts b/packages/backend/server/src/modules/storage/wrappers/blob.ts index 80b2b9bc70..30a44ef69f 100644 --- a/packages/backend/server/src/modules/storage/wrappers/blob.ts +++ b/packages/backend/server/src/modules/storage/wrappers/blob.ts @@ -1,7 +1,4 @@ -import { Readable } from 'node:stream'; - -import type { Storage } from '@affine/storage'; -import { Injectable, OnModuleInit } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import type { BlobInputType, @@ -12,20 +9,13 @@ import { Config, createStorageProvider, EventEmitter, - OctoBaseStorageModule, OnEvent, - toBuffer, } from '../../../fundamentals'; @Injectable() -export class WorkspaceBlobStorage implements OnModuleInit { +export class WorkspaceBlobStorage { public readonly provider: StorageProvider; - /** - * @deprecated for backwards compatibility, need to be removed in next stable release - */ - private octobase: Storage | null = null; - constructor( private readonly event: EventEmitter, private readonly config: Config @@ -33,42 +23,12 @@ export class WorkspaceBlobStorage implements OnModuleInit { this.provider = createStorageProvider(this.config.storage, 'blob'); } - async onModuleInit() { - if (!this.config.node.test) { - this.octobase = await OctoBaseStorageModule.Storage.connect( - this.config.db.url - ); - } - } - async put(workspaceId: string, key: string, blob: BlobInputType) { - const buf = await toBuffer(blob); - await this.provider.put(`${workspaceId}/${key}`, buf); - if (this.octobase) { - await this.octobase.uploadBlob(workspaceId, buf); - } + await this.provider.put(`${workspaceId}/${key}`, blob); } async get(workspaceId: string, key: string) { - const result = await this.provider.get(`${workspaceId}/${key}`); - if (!result.body && this.octobase) { - const blob = await this.octobase.getBlob(workspaceId, key); - - if (!blob) { - return result; - } - - return { - body: Readable.from(blob.data), - metadata: { - contentType: blob.contentType, - contentLength: blob.size, - lastModified: new Date(blob.lastModified), - }, - }; - } - - return result; + return this.provider.get(`${workspaceId}/${key}`); } async list(workspaceId: string) { From bf88b6edaa7b27767c519f1477d4be6a758f2f4a Mon Sep 17 00:00:00 2001 From: liuyi Date: Wed, 17 Jan 2024 10:37:22 +0000 Subject: [PATCH 33/92] chore(server): remove too verbose logs (#5555) chore(server): remove too verbose logs chore(server): make logs less verbose --- .../backend/server/src/fundamentals/cache/interceptor.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/backend/server/src/fundamentals/cache/interceptor.ts b/packages/backend/server/src/fundamentals/cache/interceptor.ts index 93cd388273..b865264b55 100644 --- a/packages/backend/server/src/fundamentals/cache/interceptor.ts +++ b/packages/backend/server/src/fundamentals/cache/interceptor.ts @@ -40,9 +40,9 @@ export class CacheInterceptor implements NestInterceptor { ); if (preventKey) { - this.logger.debug(`prevent cache: ${JSON.stringify(preventKey)}`); const key = await this.getCacheKey(ctx, preventKey); if (key) { + this.logger.debug(`cache ${key} staled`); await this.cache.delete(key); } @@ -60,12 +60,12 @@ export class CacheInterceptor implements NestInterceptor { const cachedData = await this.cache.get(cacheKey); if (cachedData) { - this.logger.debug('cache hit', cacheKey, cachedData); + this.logger.debug(`cache ${cacheKey} hit`); return of(cachedData); } else { + this.logger.debug(`cache ${cacheKey} miss`); return next.handle().pipe( mergeMap(async result => { - this.logger.debug('cache miss', cacheKey, result); await this.cache.set(cacheKey, result); return result; From b9f20877d0f6dcbaffe501828f816b170101478d Mon Sep 17 00:00:00 2001 From: liuyi Date: Wed, 17 Jan 2024 11:13:58 +0000 Subject: [PATCH 34/92] feat(core): make password sigin default if user has one (#5577) --- .../src/components/auth-components/modal.tsx | 2 +- .../affine/auth/after-sign-in-send-email.tsx | 41 +++++---- .../affine/auth/after-sign-up-send-email.tsx | 20 +++-- .../src/components/affine/auth/send-email.tsx | 2 +- .../affine/auth/sign-in-with-password.tsx | 90 ++++++++++++++----- .../src/components/affine/auth/sign-in.tsx | 22 ++--- .../src/components/affine/auth/style.css.ts | 35 +++++++- .../src/components/affine/auth/use-auth.ts | 9 ++ packages/frontend/i18n/src/resources/en.json | 9 +- .../frontend/i18n/src/resources/zh-Hans.json | 6 +- .../frontend/i18n/src/resources/zh-Hant.json | 2 +- tests/kit/utils/cloud.ts | 3 - 12 files changed, 170 insertions(+), 71 deletions(-) diff --git a/packages/frontend/component/src/components/auth-components/modal.tsx b/packages/frontend/component/src/components/auth-components/modal.tsx index 8561963800..8406754b62 100644 --- a/packages/frontend/component/src/components/auth-components/modal.tsx +++ b/packages/frontend/component/src/components/auth-components/modal.tsx @@ -17,7 +17,7 @@ export const AuthModal: FC> = ({ open={open} onOpenChange={setOpen} width={400} - height={468} + minHeight={500} contentOptions={{ ['data-testid' as string]: 'auth-modal', style: { padding: '44px 40px 0' }, diff --git a/packages/frontend/core/src/components/affine/auth/after-sign-in-send-email.tsx b/packages/frontend/core/src/components/affine/auth/after-sign-in-send-email.tsx index e3bdcf220f..c4c728122d 100644 --- a/packages/frontend/core/src/components/affine/auth/after-sign-in-send-email.tsx +++ b/packages/frontend/core/src/components/affine/auth/after-sign-in-send-email.tsx @@ -53,9 +53,12 @@ export const AfterSignInSendEmail = ({ subTitle={t['com.affine.auth.sign.in.sent.email.subtitle']()} /> - {t['com.affine.auth.sign.sent.email.message.start']()} - {email} - {t['com.affine.auth.sign.sent.email.message.end']()} + }} + /> + {t['com.affine.auth.sign.sent.email.message.sent-tips.sign-in']()}
@@ -73,15 +76,15 @@ export const AfterSignInSendEmail = ({ ) : ( - <> - - {t['com.affine.auth.sign.auth.code.on.resend.hint']()} - +
+
+ {t['com.affine.auth.sent']()} +
- +
)}
@@ -90,16 +93,18 @@ export const AfterSignInSendEmail = ({ {subscriptionData ? null : ( // If with payment, just support email sign in to avoid duplicate redirect to the same stripe url.   - {/*prettier-ignore*/} - - Or - sign in with password - instead. - + + ), + }} + /> )} diff --git a/packages/frontend/core/src/components/affine/auth/after-sign-up-send-email.tsx b/packages/frontend/core/src/components/affine/auth/after-sign-up-send-email.tsx index fd7c754bba..99890b0be7 100644 --- a/packages/frontend/core/src/components/affine/auth/after-sign-up-send-email.tsx +++ b/packages/frontend/core/src/components/affine/auth/after-sign-up-send-email.tsx @@ -6,6 +6,7 @@ import { } from '@affine/component/auth-components'; import { Button } from '@affine/component/ui/button'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; +import { Trans } from '@affine/i18n'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { type FC, useCallback } from 'react'; @@ -43,9 +44,12 @@ export const AfterSignUpSendEmail: FC = ({ subTitle={t['com.affine.auth.sign.up.sent.email.subtitle']()} /> - {t['com.affine.auth.sign.sent.email.message.start']()} - {email} - {t['com.affine.auth.sign.sent.email.message.end']()} + }} + /> + {t['com.affine.auth.sign.sent.email.message.sent-tips.sign-up']()}
@@ -63,15 +67,15 @@ export const AfterSignUpSendEmail: FC = ({ ) : ( - <> - - {t['com.affine.auth.sign.auth.code.on.resend.hint']()} - +
+
+ {t['com.affine.auth.sent']()} +
- +
)}
diff --git a/packages/frontend/core/src/components/affine/auth/send-email.tsx b/packages/frontend/core/src/components/affine/auth/send-email.tsx index c96d7934a6..91b3a6330b 100644 --- a/packages/frontend/core/src/components/affine/auth/send-email.tsx +++ b/packages/frontend/core/src/components/affine/auth/send-email.tsx @@ -39,7 +39,7 @@ const useContent = (emailType: AuthPanelProps['emailType'], email: string) => { case 'setPassword': return t['com.affine.auth.set.password.message'](); case 'changePassword': - return t['com.affine.auth.set.password.message'](); + return t['com.affine.auth.reset.password.message'](); case 'changeEmail': return t['com.affine.auth.change.email.message']({ email, diff --git a/packages/frontend/core/src/components/affine/auth/sign-in-with-password.tsx b/packages/frontend/core/src/components/affine/auth/sign-in-with-password.tsx index 2d4e3312f9..bb649bd9d8 100644 --- a/packages/frontend/core/src/components/affine/auth/sign-in-with-password.tsx +++ b/packages/frontend/core/src/components/affine/auth/sign-in-with-password.tsx @@ -14,10 +14,13 @@ import { useCallback, useState } from 'react'; import { signInCloud } from '../../../utils/cloud-utils'; import type { AuthPanelProps } from './index'; -import { forgetPasswordButton } from './style.css'; +import * as styles from './style.css'; +import { INTERNAL_BETA_URL, useAuth } from './use-auth'; +import { useCaptcha } from './use-captcha'; export const SignInWithPassword: FC = ({ setAuthState, + setEmailType, email, onSignedIn, }) => { @@ -26,6 +29,13 @@ export const SignInWithPassword: FC = ({ const [password, setPassword] = useState(''); const [passwordError, setPasswordError] = useState(false); + const { + signIn, + allowSendEmail, + resetCountDown, + isMutating: sendingEmail, + } = useAuth(); + const [verifyToken, challenge] = useCaptcha(); const onSignIn = useAsyncCallback(async () => { const res = await signInCloud('credentials', { @@ -42,6 +52,31 @@ export const SignInWithPassword: FC = ({ onSignedIn?.(); }, [email, password, onSignedIn, update]); + const sendMagicLink = useAsyncCallback(async () => { + if (allowSendEmail && verifyToken && !sendingEmail) { + const res = await signIn(email, verifyToken, challenge); + if (res?.status === 403 && res?.url === INTERNAL_BETA_URL) { + resetCountDown(); + return setAuthState('noAccess'); + } + setAuthState('afterSignInSendEmail'); + } + }, [ + email, + signIn, + allowSendEmail, + sendingEmail, + setAuthState, + verifyToken, + challenge, + resetCountDown, + ]); + + const sendChangePasswordEmail = useCallback(() => { + setEmailType('changePassword'); + setAuthState('sendEmail'); + }, [setAuthState, setEmailType]); + return ( <> = ({ = ({ errorHint={t['com.affine.auth.password.error']()} onEnter={onSignIn} /> - - + + {t['com.affine.auth.forget']()} + + + + - - { - setAuthState('afterSignInSendEmail'); + setAuthState('signIn'); }, [setAuthState])} /> diff --git a/packages/frontend/core/src/components/affine/auth/sign-in.tsx b/packages/frontend/core/src/components/affine/auth/sign-in.tsx index f3885a157c..a43d7b7c06 100644 --- a/packages/frontend/core/src/components/affine/auth/sign-in.tsx +++ b/packages/frontend/core/src/components/affine/auth/sign-in.tsx @@ -61,16 +61,13 @@ export const SignIn: FC = ({ setIsValidEmail(true); // 0 for no access for internal beta - let user: GetUserQuery['user'] | null | 0 = null; - await verifyUser({ email }) - .then(({ user: u }) => { - user = u; - }) + const user: GetUserQuery['user'] | null | 0 = await verifyUser({ email }) + .then(({ user }) => user) .catch(err => { const e = err?.[0]; if (e instanceof GraphQLError && e.extensions?.code === 402) { setAuthState('noAccess'); - user = 0; + return 0; } else { throw err; } @@ -83,11 +80,16 @@ export const SignIn: FC = ({ if (verifyToken) { if (user) { - const res = await signIn(email, verifyToken, challenge); - if (res?.status === 403 && res?.url === INTERNAL_BETA_URL) { - return setAuthState('noAccess'); + // provider password sign-in if user has by default + if (user.hasPassword) { + setAuthState('signInWithPassword'); + } else { + const res = await signIn(email, verifyToken, challenge); + if (res?.status === 403 && res?.url === INTERNAL_BETA_URL) { + return setAuthState('noAccess'); + } + setAuthState('afterSignInSendEmail'); } - setAuthState('afterSignInSendEmail'); } else { const res = await signUp(email, verifyToken, challenge); if (res?.status === 403 && res?.url === INTERNAL_BETA_URL) { diff --git a/packages/frontend/core/src/components/affine/auth/style.css.ts b/packages/frontend/core/src/components/affine/auth/style.css.ts index a8331464f8..e03f570396 100644 --- a/packages/frontend/core/src/components/affine/auth/style.css.ts +++ b/packages/frontend/core/src/components/affine/auth/style.css.ts @@ -24,13 +24,30 @@ globalStyle(`${authMessage} .link`, { color: 'var(--affine-link-color)', }); +export const forgetPasswordButtonRow = style({ + position: 'absolute', + right: 0, + marginTop: '-26px', // Let this button be a tail of password input. +}); +export const sendMagicLinkButtonRow = style({ + marginBottom: '30px', +}); + +export const linkButton = style({ + color: 'var(--affine-link-color)', + background: 'transparent', + borderColor: 'transparent', + fontSize: 'var(--affine-font-xs)', + lineHeight: '22px', + userSelect: 'none', +}); + export const forgetPasswordButton = style({ fontSize: 'var(--affine-font-sm)', color: 'var(--affine-text-secondary-color)', position: 'absolute', right: 0, bottom: 0, - display: 'none', }); export const resendWrapper = style({ @@ -42,7 +59,21 @@ export const resendWrapper = style({ marginTop: 30, }); -export const resendCountdown = style({ width: 45, textAlign: 'center' }); +export const sentRow = style({ + display: 'flex', + justifyContent: 'center', + gap: '8px', + lineHeight: '22px', + fontSize: 'var(--affine-font-sm)', +}); +export const sentMessage = style({ + color: 'var(--affine-text-primary-color)', + fontWeight: 600, +}); +export const resendCountdown = style({ + width: 45, + textAlign: 'center', +}); export const resendCountdownInButton = style({ width: 40, textAlign: 'center', diff --git a/packages/frontend/core/src/components/affine/auth/use-auth.ts b/packages/frontend/core/src/components/affine/auth/use-auth.ts index e2b338b010..dac05ec1fd 100644 --- a/packages/frontend/core/src/components/affine/auth/use-auth.ts +++ b/packages/frontend/core/src/components/affine/auth/use-auth.ts @@ -150,9 +150,18 @@ export const useAuth = () => { signInCloud('google').catch(console.error); }, []); + const resetCountDown = useCallback(() => { + setAuthStore({ + isMutating: false, + allowSendEmail: false, + resendCountDown: 0, + }); + }, [setAuthStore]); + return { allowSendEmail: authStore.allowSendEmail, resendCountDown: authStore.resendCountDown, + resetCountDown, isMutating: authStore.isMutating, signUp, signIn, diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index b99ad542b9..34fdb49079 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -454,8 +454,8 @@ "com.affine.auth.sign.auth.code.error.hint": "Wrong code, please try again", "com.affine.auth.sign.auth.code.message": "If you haven't received the email, please check your spam folder.", "com.affine.auth.sign.auth.code.message.password": "Or <1>sign in with password instead.", - "com.affine.auth.sign.auth.code.on.resend.hint": "Send code again", - "com.affine.auth.sign.auth.code.resend.hint": "Resend code", + "com.affine.auth.sign.auth.code.resend.hint": "Resend link", + "com.affine.auth.sign.auth.code.send-email.sign-in": "Sign in with magic link", "com.affine.auth.sign.condition": "Terms of Conditions", "com.affine.auth.sign.email.continue": "Continue with Email", "com.affine.auth.sign.email.error": "Invalid email", @@ -467,8 +467,9 @@ "com.affine.auth.sign.no.access.link": "AFFiNE Cloud Early Access", "com.affine.auth.sign.no.access.wait": "Wait for public release", "com.affine.auth.sign.policy": "Privacy Policy", - "com.affine.auth.sign.sent.email.message.end": " You can click the link to create an account automatically.", - "com.affine.auth.sign.sent.email.message.start": "An email with a magic link has been sent to ", + "com.affine.auth.sign.sent.email.message.sent-tips": "An email with a magic link has been sent to {{email}}.", + "com.affine.auth.sign.sent.email.message.sent-tips.sign-up": "You can click the link to create an account automatically.", + "com.affine.auth.sign.sent.email.message.sent-tips.sign-in": "You can click the link to sign in automatically.", "com.affine.auth.sign.up": "Sign up", "com.affine.auth.sign.up.sent.email.subtitle": "Create your account", "com.affine.auth.sign.up.success.subtitle": "The app will automatically open or redirect to the web version. If you encounter any issues, you can also click the button below to manually open the AFFiNE app.", diff --git a/packages/frontend/i18n/src/resources/zh-Hans.json b/packages/frontend/i18n/src/resources/zh-Hans.json index 54a6b9c7a7..4a02b375d9 100644 --- a/packages/frontend/i18n/src/resources/zh-Hans.json +++ b/packages/frontend/i18n/src/resources/zh-Hans.json @@ -443,7 +443,8 @@ "com.affine.auth.sign.auth.code.message": "如果您没有收到电子邮件,请检查您的垃圾邮件文件夹。", "com.affine.auth.sign.auth.code.message.password": "或者使用<1>密码登录。", "com.affine.auth.sign.auth.code.on.resend.hint": "再次发送验证码", - "com.affine.auth.sign.auth.code.resend.hint": "重发验证码", + "com.affine.auth.sign.auth.code.sent-tips": "重发验证码", + "com.affine.auth.sign.auth.code.send-email.sign-in": "邮件登录", "com.affine.auth.sign.condition": "条款与条件", "com.affine.auth.sign.email.continue": "以电子邮件继续", "com.affine.auth.sign.email.error": "无效的电子邮件", @@ -456,7 +457,8 @@ "com.affine.auth.sign.no.access.wait": "请期待正式发行", "com.affine.auth.sign.policy": "隐私政策", "com.affine.auth.sign.sent.email.message.end": "您可以点击链接自动创建账户。", - "com.affine.auth.sign.sent.email.message.start": "一封带有 magic link 的邮件已发送至", + "com.affine.auth.sign.sent.email.message.sign-in.end": "您可以点击链接自动登录。", + "com.affine.auth.sign.sent.email.message.sent-tips": "一封带有 magic link 的邮件已发送至 {{email}}。", "com.affine.auth.sign.up": "注册", "com.affine.auth.sign.up.sent.email.subtitle": "创建您的账号", "com.affine.auth.sign.up.success.subtitle": "AFFiNE 客户端将自动打开或重定向到网页端,如果您遇到任何问题,可以点击下方按钮手动打开 AFFiNE。", diff --git a/packages/frontend/i18n/src/resources/zh-Hant.json b/packages/frontend/i18n/src/resources/zh-Hant.json index 32e6b38a33..e69f3064fb 100644 --- a/packages/frontend/i18n/src/resources/zh-Hant.json +++ b/packages/frontend/i18n/src/resources/zh-Hant.json @@ -365,7 +365,7 @@ "com.affine.auth.sign.message": "點擊上方「使用 Google/電子郵件地址 繼續」,即表示您確認同意 AFFiNE 的 <1>Terms of Conditions and <3>Privacy Policy。", "com.affine.auth.sign.policy": "隱私政策", "com.affine.auth.sign.sent.email.message.end": "您可以點擊連結自動建立帳號。", - "com.affine.auth.sign.sent.email.message.start": "一封帶有魔法連結的電子郵件已發送至", + "com.affine.auth.sign.sent.email.message.sent-tips": "一封帶有魔法連結的電子郵件已發送至 {{email}}。", "com.affine.auth.sign.up": "建立帳號", "com.affine.auth.sign.up.sent.email.subtitle": "建立您的帳號", "com.affine.auth.sign.up.success.subtitle": "應用將自動打開或重定向到網頁版本。如果您遇到任何問題,您還可以點擊下面的按鈕手動打開 AFFiNE 應用程序。", diff --git a/tests/kit/utils/cloud.ts b/tests/kit/utils/cloud.ts index 0fe780780a..7a54d1e91c 100644 --- a/tests/kit/utils/cloud.ts +++ b/tests/kit/utils/cloud.ts @@ -170,9 +170,6 @@ export async function loginUser( await page.getByTestId('continue-login-button').click({ delay: 200, }); - await page.getByTestId('sign-in-with-password').click({ - delay: 200, - }); await page.getByTestId('password-input').fill('123456'); if (config?.beforeLogin) { await config.beforeLogin(); From ee8ec47a4fc966c2183bcffcaa30c494c10b7725 Mon Sep 17 00:00:00 2001 From: DarkSky Date: Wed, 17 Jan 2024 12:36:21 +0000 Subject: [PATCH 35/92] feat: use `SafeInt` replace `Float` (#5613) --- packages/backend/server/package.json | 1 + .../backend/server/src/modules/users/types.ts | 9 +++++---- .../src/modules/workspaces/resolvers/blob.ts | 4 ++-- .../server/src/modules/workspaces/types.ts | 4 ++-- packages/backend/server/src/schema.gql | 15 ++++++++++----- packages/backend/server/tests/utils/blobs.ts | 2 +- packages/frontend/graphql/codegen.yml | 3 ++- .../graphql/src/graphql/blob-check-size.gql | 2 +- packages/frontend/graphql/src/graphql/index.ts | 2 +- packages/frontend/graphql/src/schema.ts | 4 +++- yarn.lock | 12 ++++++++++++ 11 files changed, 40 insertions(+), 18 deletions(-) diff --git a/packages/backend/server/package.json b/packages/backend/server/package.json index cffb49d4d8..059f34ba7d 100644 --- a/packages/backend/server/package.json +++ b/packages/backend/server/package.json @@ -63,6 +63,7 @@ "file-type": "^19.0.0", "get-stream": "^8.0.1", "graphql": "^16.8.1", + "graphql-scalars": "^1.22.4", "graphql-type-json": "^0.3.2", "graphql-upload": "^16.0.2", "ioredis": "^5.3.2", diff --git a/packages/backend/server/src/modules/users/types.ts b/packages/backend/server/src/modules/users/types.ts index 0da51645af..eb41b31618 100644 --- a/packages/backend/server/src/modules/users/types.ts +++ b/packages/backend/server/src/modules/users/types.ts @@ -1,5 +1,6 @@ -import { createUnionType, Field, Float, ID, ObjectType } from '@nestjs/graphql'; +import { createUnionType, Field, ID, ObjectType } from '@nestjs/graphql'; import type { User } from '@prisma/client'; +import { SafeIntResolver } from 'graphql-scalars'; @ObjectType('UserQuotaHumanReadable') export class UserQuotaHumanReadableType { @@ -24,13 +25,13 @@ export class UserQuotaType { @Field({ name: 'name' }) name!: string; - @Field(() => Float, { name: 'blobLimit' }) + @Field(() => SafeIntResolver, { name: 'blobLimit' }) blobLimit!: number; - @Field(() => Float, { name: 'storageQuota' }) + @Field(() => SafeIntResolver, { name: 'storageQuota' }) storageQuota!: number; - @Field(() => Float, { name: 'historyPeriod' }) + @Field(() => SafeIntResolver, { name: 'historyPeriod' }) historyPeriod!: number; @Field({ name: 'memberLimit' }) diff --git a/packages/backend/server/src/modules/workspaces/resolvers/blob.ts b/packages/backend/server/src/modules/workspaces/resolvers/blob.ts index f1d960d74a..7f3cba8b12 100644 --- a/packages/backend/server/src/modules/workspaces/resolvers/blob.ts +++ b/packages/backend/server/src/modules/workspaces/resolvers/blob.ts @@ -1,7 +1,6 @@ import { HttpStatus, Logger, UseGuards } from '@nestjs/common'; import { Args, - Float, Int, Mutation, Parent, @@ -10,6 +9,7 @@ import { Resolver, } from '@nestjs/graphql'; import { GraphQLError } from 'graphql'; +import { SafeIntResolver } from 'graphql-scalars'; import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs'; import { @@ -100,7 +100,7 @@ export class WorkspaceBlobResolver { async checkBlobSize( @CurrentUser() user: UserType, @Args('workspaceId') workspaceId: string, - @Args('size', { type: () => Float }) blobSize: number + @Args('size', { type: () => SafeIntResolver }) blobSize: number ) { const canWrite = await this.permissions.tryCheckWorkspace( workspaceId, diff --git a/packages/backend/server/src/modules/workspaces/types.ts b/packages/backend/server/src/modules/workspaces/types.ts index c3492f4230..e959a1d1f4 100644 --- a/packages/backend/server/src/modules/workspaces/types.ts +++ b/packages/backend/server/src/modules/workspaces/types.ts @@ -1,6 +1,5 @@ import { Field, - Float, ID, InputType, ObjectType, @@ -10,6 +9,7 @@ import { registerEnumType, } from '@nestjs/graphql'; import type { Workspace } from '@prisma/client'; +import { SafeIntResolver } from 'graphql-scalars'; import { UserType } from '../users/types'; @@ -78,7 +78,7 @@ export class InvitationWorkspaceType { @ObjectType() export class WorkspaceBlobSizes { - @Field(() => Float) + @Field(() => SafeIntResolver) size!: number; } diff --git a/packages/backend/server/src/schema.gql b/packages/backend/server/src/schema.gql index c630dd064b..b6af02d7b6 100644 --- a/packages/backend/server/src/schema.gql +++ b/packages/backend/server/src/schema.gql @@ -23,13 +23,18 @@ type UserQuotaHumanReadable { type UserQuota { name: String! - blobLimit: Float! - storageQuota: Float! - historyPeriod: Float! + blobLimit: SafeInt! + storageQuota: SafeInt! + historyPeriod: SafeInt! memberLimit: Int! humanReadable: UserQuotaHumanReadable! } +""" +The `SafeInt` scalar type represents non-fractional signed whole numeric values that are considered safe as defined by the ECMAScript specification. +""" +scalar SafeInt @specifiedBy(url: "https://www.ecma-international.org/ecma-262/#sec-number.issafeinteger") + type UserType { id: ID! @@ -172,7 +177,7 @@ type InvitationWorkspaceType { } type WorkspaceBlobSizes { - size: Float! + size: SafeInt! } type InvitationType { @@ -309,7 +314,7 @@ type Query { """List blobs of workspace""" listBlobs(workspaceId: String!): [String!]! @deprecated(reason: "use `workspace.blobs` instead") collectAllBlobSizes: WorkspaceBlobSizes! @deprecated(reason: "use `user.storageUsage` instead") - checkBlobSize(workspaceId: String!, size: Float!): WorkspaceBlobSizes! @deprecated(reason: "no more needed") + checkBlobSize(workspaceId: String!, size: SafeInt!): WorkspaceBlobSizes! @deprecated(reason: "no more needed") """Get current user""" currentUser: UserType diff --git a/packages/backend/server/tests/utils/blobs.ts b/packages/backend/server/tests/utils/blobs.ts index 5d13797634..514fbeee46 100644 --- a/packages/backend/server/tests/utils/blobs.ts +++ b/packages/backend/server/tests/utils/blobs.ts @@ -74,7 +74,7 @@ export async function checkBlobSize( .post(gql) .auth(token, { type: 'bearer' }) .send({ - query: `query checkBlobSize($workspaceId: String!, $size: Float!) { + query: `query checkBlobSize($workspaceId: String!, $size: SafeInt!) { checkBlobSize(workspaceId: $workspaceId, size: $size) { size } diff --git a/packages/frontend/graphql/codegen.yml b/packages/frontend/graphql/codegen.yml index c362371cf3..6faebf03a4 100644 --- a/packages/frontend/graphql/codegen.yml +++ b/packages/frontend/graphql/codegen.yml @@ -16,8 +16,9 @@ config: Decimal: number UUID: string ID: string - JSON: any + JSON: string Upload: File + SafeInt: number overwrite: true schema: ../../backend/server/src/schema.gql documents: ./src/**/*.gql diff --git a/packages/frontend/graphql/src/graphql/blob-check-size.gql b/packages/frontend/graphql/src/graphql/blob-check-size.gql index be76a0a1ab..db122f8df6 100644 --- a/packages/frontend/graphql/src/graphql/blob-check-size.gql +++ b/packages/frontend/graphql/src/graphql/blob-check-size.gql @@ -1,4 +1,4 @@ -query checkBlobSizes($workspaceId: String!, $size: Float!) { +query checkBlobSizes($workspaceId: String!, $size: SafeInt!) { checkBlobSize(workspaceId: $workspaceId, size: $size) { size } diff --git a/packages/frontend/graphql/src/graphql/index.ts b/packages/frontend/graphql/src/graphql/index.ts index a4b85368b9..f357782539 100644 --- a/packages/frontend/graphql/src/graphql/index.ts +++ b/packages/frontend/graphql/src/graphql/index.ts @@ -13,7 +13,7 @@ export const checkBlobSizesQuery = { definitionName: 'checkBlobSize', containsFile: false, query: ` -query checkBlobSizes($workspaceId: String!, $size: Float!) { +query checkBlobSizes($workspaceId: String!, $size: SafeInt!) { checkBlobSize(workspaceId: $workspaceId, size: $size) { size } diff --git a/packages/frontend/graphql/src/schema.ts b/packages/frontend/graphql/src/schema.ts index 557a10dced..fb9ad9beae 100644 --- a/packages/frontend/graphql/src/schema.ts +++ b/packages/frontend/graphql/src/schema.ts @@ -28,6 +28,8 @@ export interface Scalars { Float: { input: number; output: number }; /** A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format. */ DateTime: { input: string; output: string }; + /** The `SafeInt` scalar type represents non-fractional signed whole numeric values that are considered safe as defined by the ECMAScript specification. */ + SafeInt: { input: number; output: number }; /** The `Upload` scalar type represents a file upload. */ Upload: { input: File; output: File }; } @@ -93,7 +95,7 @@ export interface UpdateWorkspaceInput { export type CheckBlobSizesQueryVariables = Exact<{ workspaceId: Scalars['String']['input']; - size: Scalars['Float']['input']; + size: Scalars['SafeInt']['input']; }>; export type CheckBlobSizesQuery = { diff --git a/yarn.lock b/yarn.lock index 1f9e6e3211..e57f9801d2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -689,6 +689,7 @@ __metadata: file-type: "npm:^19.0.0" get-stream: "npm:^8.0.1" graphql: "npm:^16.8.1" + graphql-scalars: "npm:^1.22.4" graphql-type-json: "npm:^0.3.2" graphql-upload: "npm:^16.0.2" ioredis: "npm:^5.3.2" @@ -21878,6 +21879,17 @@ __metadata: languageName: node linkType: hard +"graphql-scalars@npm:^1.22.4": + version: 1.22.4 + resolution: "graphql-scalars@npm:1.22.4" + dependencies: + tslib: "npm:^2.5.0" + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + checksum: 110c1fa29d7564d9967c974de2df08c99d7ed43af98097e5a5729f952c064b21f66a9f5a68143c50eeac6a4ce14f0999aed2ce8f043d02331d133b0c59a9c1ec + languageName: node + linkType: hard + "graphql-tag@npm:2.12.6, graphql-tag@npm:^2.11.0": version: 2.12.6 resolution: "graphql-tag@npm:2.12.6" From 2b92b27f8f9fd2bc86bf04dd19a5520a2211fb47 Mon Sep 17 00:00:00 2001 From: Cats Juice Date: Thu, 18 Jan 2024 02:14:27 +0000 Subject: [PATCH 36/92] feat(component): new week-date-picker component (#5477) --- .../component/src/ui/date-picker/index.ts | 1 + .../ui/date-picker/week-date-picker.css.tsx | 101 +++++++ .../date-picker/week-date-picker.stories.tsx | 48 ++++ .../src/ui/date-picker/week-date-picker.tsx | 249 ++++++++++++++++++ .../src/ui/resize-panel/resize-panel.tsx | 122 +++++++++ .../src/ui/resize-panel/styles.css.ts | 26 ++ packages/frontend/i18n/src/resources/en.json | 9 +- 7 files changed, 555 insertions(+), 1 deletion(-) create mode 100644 packages/frontend/component/src/ui/date-picker/week-date-picker.css.tsx create mode 100644 packages/frontend/component/src/ui/date-picker/week-date-picker.stories.tsx create mode 100644 packages/frontend/component/src/ui/date-picker/week-date-picker.tsx create mode 100644 packages/frontend/component/src/ui/resize-panel/resize-panel.tsx create mode 100644 packages/frontend/component/src/ui/resize-panel/styles.css.ts diff --git a/packages/frontend/component/src/ui/date-picker/index.ts b/packages/frontend/component/src/ui/date-picker/index.ts index e5ce37c674..691bfd7ada 100644 --- a/packages/frontend/component/src/ui/date-picker/index.ts +++ b/packages/frontend/component/src/ui/date-picker/index.ts @@ -1 +1,2 @@ export * from './date-picker'; +export * from './week-date-picker'; diff --git a/packages/frontend/component/src/ui/date-picker/week-date-picker.css.tsx b/packages/frontend/component/src/ui/date-picker/week-date-picker.css.tsx new file mode 100644 index 0000000000..6b7442d74f --- /dev/null +++ b/packages/frontend/component/src/ui/date-picker/week-date-picker.css.tsx @@ -0,0 +1,101 @@ +import { style } from '@vanilla-extract/css'; + +export const weekDatePicker = style({ + display: 'flex', + alignItems: 'center', + gap: 4, + + height: '100%', + maxHeight: '39px', +}); + +export const weekDatePickerContent = style({ + width: 0, + flex: 1, + display: 'flex', + alignItems: 'stretch', + justifyContent: 'space-between', + gap: 4, + userSelect: 'none', +}); + +export const dayCell = style({ + position: 'relative', + width: 0, + flexGrow: 1, + minWidth: 30, + maxWidth: 130, + + cursor: 'pointer', + + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + + padding: '2px 4px 1px 4px', + borderRadius: 4, + + fontFamily: 'var(--affine-font-family)', + fontWeight: 500, + fontSize: 12, + + selectors: { + '&:hover': { + backgroundColor: 'var(--affine-hover-color)', + }, + '&[data-today="true"]:not([data-active="true"])': { + vars: { + '--cell-color': 'var(--affine-brand-color)', + }, + }, + '&[data-active="true"]': { + vars: { + '--cell-color': 'var(--affine-pure-white)', + }, + background: 'var(--affine-brand-color)', + }, + + // interactive + '&::before, &::after': { + content: '', + position: 'absolute', + inset: 0, + zIndex: 1, + pointerEvents: 'none', + borderRadius: 'inherit', + opacity: 0, + }, + '&::before': { + boxShadow: '0 0 0 2px var(--affine-brand-color)', + }, + '&::after': { + border: '1px solid var(--affine-brand-color)', + }, + '&:focus-visible::before': { + opacity: 0.5, + }, + '&:focus-visible::after': { + opacity: 1, + }, + }, +}); +export const dayCellWeek = style({ + width: '100%', + height: 16, + lineHeight: '16px', + textAlign: 'center', + + textOverflow: 'clip', + whiteSpace: 'nowrap', + overflow: 'hidden', + color: 'var(--cell-color, var(--affine-text-secondary-color))', +}); +export const dayCellDate = style({ + width: '100%', + height: 20, + lineHeight: '20px', + textAlign: 'center', + + color: 'var(--cell-color, var(--affine-text-primary-color))', +}); diff --git a/packages/frontend/component/src/ui/date-picker/week-date-picker.stories.tsx b/packages/frontend/component/src/ui/date-picker/week-date-picker.stories.tsx new file mode 100644 index 0000000000..4beb72111a --- /dev/null +++ b/packages/frontend/component/src/ui/date-picker/week-date-picker.stories.tsx @@ -0,0 +1,48 @@ +import type { Meta, StoryFn } from '@storybook/react'; +import dayjs from 'dayjs'; +import { useState } from 'react'; + +import { ResizePanel } from '../resize-panel/resize-panel'; +import { WeekDatePicker } from './week-date-picker'; + +export default { + title: 'UI/Date Picker/Week Date Picker', +} satisfies Meta; + +const _format = 'YYYY-MM-DD'; + +const Template: StoryFn = args => { + const [date, setDate] = useState(dayjs().format(_format)); + return ( +
+
Selected Date: {date}
+ + + { + setDate(dayjs(e, _format).format(_format)); + }} + /> + +
+ ); +}; + +export const Basic: StoryFn = Template.bind(undefined); +Basic.args = {}; diff --git a/packages/frontend/component/src/ui/date-picker/week-date-picker.tsx b/packages/frontend/component/src/ui/date-picker/week-date-picker.tsx new file mode 100644 index 0000000000..68e1f53cc9 --- /dev/null +++ b/packages/frontend/component/src/ui/date-picker/week-date-picker.tsx @@ -0,0 +1,249 @@ +import { ArrowLeftSmallIcon, ArrowRightSmallIcon } from '@blocksuite/icons'; +import clsx from 'clsx'; +import dayjs from 'dayjs'; +import { + type ForwardedRef, + type HTMLAttributes, + memo, + useCallback, + useEffect, + useImperativeHandle, + useRef, + useState, +} from 'react'; + +import { IconButton } from '../button'; +import * as styles from './week-date-picker.css'; + +export interface WeekDatePickerHandle { + /** control cursor manually */ + setCursor?: (cursor: dayjs.Dayjs) => void; +} + +export interface WeekDatePickerProps + extends Omit, 'onChange'> { + value?: string; + onChange?: (value: string) => void; + handleRef?: ForwardedRef; +} + +// TODO: i18n +const weekDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; +// const weekDays = ['日', '一', '二', '三', '四', '五', '六']; +const format = 'YYYY-MM-DD'; + +export const WeekDatePicker = memo(function WeekDatePicker({ + value, + onChange, + className, + handleRef, + ...attrs +}: WeekDatePickerProps) { + const weekRef = useRef(null); + + const [cursor, setCursor] = useState(dayjs(value)); + const [range, setRange] = useState([0, 7]); + const [dense, setDense] = useState(false); + const [allDays, setAllDays] = useState([]); + const [viewPortSize, setViewPortSize] = useState(7); + + useImperativeHandle(handleRef, () => ({ + setCursor, + })); + + const displayDays = allDays.slice(...range); + + const updateRange = useCallback( + (newRange: [number, number]) => { + if (range && newRange[0] === range[0] && newRange[1] === range[1]) return; + setRange(newRange); + }, + [range] + ); + + const onNext = useCallback(() => { + if (viewPortSize === 7) { + setCursor(cursor.add(1, 'week')); + } else if (range[1] === 7) { + setCursor(cursor.add(1, 'week')); + updateRange([0, viewPortSize]); + } else { + const end = Math.min(range[1] + viewPortSize, 7); + const start = Math.min(range[0] + viewPortSize, end - viewPortSize); + updateRange([start, end]); + } + }, [cursor, range, updateRange, viewPortSize]); + const onPrev = useCallback(() => { + if (viewPortSize === 7) { + setCursor(cursor.add(-1, 'week')); + } else if (range[0] === 0) { + setCursor(cursor.add(-1, 'week')); + updateRange([7 - viewPortSize, 7]); + } else { + const start = Math.max(range[0] - viewPortSize, 0); + const end = Math.max(range[1] - viewPortSize, start + viewPortSize); + updateRange([start, end]); + } + }, [cursor, range, updateRange, viewPortSize]); + const onDayClick = useCallback( + (day: dayjs.Dayjs) => { + onChange?.(day.format(format)); + }, + [onChange] + ); + + // Observe weekRef to update viewPortSize + useEffect(() => { + const el = weekRef.current; + if (!el) return; + + const resizeObserver = new ResizeObserver(entries => { + const rect = entries[0].contentRect; + const width = rect.width; + if (!width) return; + + const minWidth = 30; + const gap = 4; + const viewPortCount = Math.floor(width / (minWidth + gap)); + setViewPortSize(Math.max(1, Math.min(viewPortCount, 7))); + setDense(width < 300); + }); + resizeObserver.observe(el); + + return () => { + resizeObserver.disconnect(); + }; + }, []); + + // update allDays when cursor changes + useEffect(() => { + const firstDay = dayjs(cursor).startOf('week'); + if (allDays[0] && firstDay.isSame(allDays[0], 'day')) return; + + setAllDays( + Array.from({ length: 7 }).map((_, index) => + firstDay.add(index, 'day').startOf('day') + ) + ); + }, [allDays, cursor]); + + // when viewPortSize changes, reset range + useEffect(() => { + if (viewPortSize >= 7) updateRange([0, 7]); + else { + const end = Math.min(7, range[0] + viewPortSize); + const start = Math.max(0, end - viewPortSize); + updateRange([start, end]); + } + }, [range, updateRange, viewPortSize]); + + // when value changes, reset cursor + useEffect(() => { + value && setCursor(dayjs(value)); + }, [value]); + + // TODO: keyboard navigation + useEffect(() => { + if (!weekRef.current) return; + const el = weekRef.current; + const onKeyDown = (e: KeyboardEvent) => { + if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return; + e.preventDefault(); + e.stopPropagation(); + + const focused = document.activeElement as HTMLElement; + if (!focused) return el.querySelector('button')?.focus(); + const day = dayjs(focused.dataset.value); + if ( + (day.day() === 0 && e.key === 'ArrowLeft') || + (e.key === 'ArrowLeft' && !focused.previousElementSibling) + ) { + onPrev(); + requestAnimationFrame(() => { + el.querySelector('button')?.focus(); + }); + } + + if ( + (day.day() === 6 && e.key === 'ArrowRight') || + (e.key === 'ArrowRight' && !focused.nextElementSibling) + ) { + onNext(); + requestAnimationFrame(() => { + (el.querySelector('button:last-child') as HTMLElement)?.focus(); + }); + } + + if (e.key === 'ArrowLeft' && focused.previousElementSibling) { + (focused.previousElementSibling as HTMLElement).focus(); + } + if (e.key === 'ArrowRight' && focused.nextElementSibling) { + (focused.nextElementSibling as HTMLElement).focus(); + } + }; + el.addEventListener('keydown', onKeyDown); + return () => { + el.removeEventListener('keydown', onKeyDown); + }; + }, [onNext, onPrev]); + + return ( +
+ + + + +
+ {displayDays.map(day => ( + + ))} +
+ + + + +
+ ); +}); + +interface CellProps { + dense: boolean; + day: dayjs.Dayjs; + cursor: dayjs.Dayjs; + value?: string; + onClick: (day: dayjs.Dayjs) => void; +} +const Cell = ({ day, dense, value, cursor, onClick }: CellProps) => { + const isActive = day.format(format) === value; + const isCurrentMonth = day.month() === cursor.month(); + const isToday = day.isSame(dayjs(), 'day'); + + const dayIndex = day.day(); + const label = weekDays[dayIndex]; + + return ( + + ); +}; diff --git a/packages/frontend/component/src/ui/resize-panel/resize-panel.tsx b/packages/frontend/component/src/ui/resize-panel/resize-panel.tsx new file mode 100644 index 0000000000..778af0c008 --- /dev/null +++ b/packages/frontend/component/src/ui/resize-panel/resize-panel.tsx @@ -0,0 +1,122 @@ +import clsx from 'clsx'; +import { + type HTMLAttributes, + type PropsWithChildren, + useEffect, + useRef, +} from 'react'; + +import * as styles from './styles.css'; + +export interface ResizePanelProps + extends PropsWithChildren, + HTMLAttributes { + horizontal?: boolean; + vertical?: boolean; + width?: number; + height?: number; + minWidth?: number; + minHeight?: number; + maxWidth?: number; + maxHeight?: number; +} + +/** + * This component is used for debugging responsive layout in storybook + * @internal + */ +export const ResizePanel = ({ + width, + height, + children, + minHeight, + minWidth, + maxHeight, + maxWidth, + className, + horizontal = true, + vertical = true, + + ...attrs +}: ResizePanelProps) => { + const containerRef = useRef(null); + const cornerHandleRef = useRef(null); + + useEffect(() => { + if (!containerRef.current || !cornerHandleRef.current) return; + + const containerEl = containerRef.current; + const cornerHandleEl = cornerHandleRef.current; + + let startPos: [number, number] = [0, 0]; + let startSize: [number, number] = [0, 0]; + + const onDragStart = (e: MouseEvent) => { + startPos = [e.clientX, e.clientY]; + startSize = [containerEl.offsetWidth, containerEl.offsetHeight]; + document.addEventListener('mousemove', onDrag); + document.addEventListener('mouseup', onDragEnd); + }; + + const onDrag = (e: MouseEvent) => { + const pos = [e.clientX, e.clientY]; + const delta = [pos[0] - startPos[0], pos[1] - startPos[1]]; + const newSize = [startSize[0] + delta[0], startSize[1] + delta[1]]; + updateSize(newSize); + }; + + const onDragEnd = () => { + document.removeEventListener('mousemove', onDrag); + document.removeEventListener('mouseup', onDragEnd); + }; + + const updateSize = (size: number[]) => { + if (!containerEl) return; + + if (horizontal) { + const width = Math.max( + Math.min(size[0], maxWidth ?? Infinity), + minWidth ?? 0 + ); + containerEl.style.width = `${width}px`; + } + + if (vertical) { + const height = Math.max( + Math.min(size[1], maxHeight ?? Infinity), + minHeight ?? 0 + ); + containerEl.style.height = `${height}px`; + } + }; + + updateSize([width ?? 400, height ?? 200]); + cornerHandleEl.addEventListener('mousedown', onDragStart); + + return () => { + cornerHandleEl.removeEventListener('mousedown', onDragStart); + document.removeEventListener('mousemove', onDrag); + document.removeEventListener('mouseup', onDragEnd); + }; + }, [ + height, + horizontal, + maxHeight, + maxWidth, + minHeight, + minWidth, + vertical, + width, + ]); + + return ( +
+ {children} +
+
+ ); +}; diff --git a/packages/frontend/component/src/ui/resize-panel/styles.css.ts b/packages/frontend/component/src/ui/resize-panel/styles.css.ts new file mode 100644 index 0000000000..bd9c81ca0b --- /dev/null +++ b/packages/frontend/component/src/ui/resize-panel/styles.css.ts @@ -0,0 +1,26 @@ +import { style } from '@vanilla-extract/css'; + +const HANDLE_SIZE = 24; + +export const container = style({ + position: 'relative', + border: '1px solid rgba(100, 100, 100, 0.2)', + padding: 8, + borderRadius: 4, + borderBottomRightRadius: HANDLE_SIZE / 2, +}); + +export const cornerHandle = style({ + position: 'absolute', + top: `calc(100% - ${HANDLE_SIZE / 1.5}px)`, + left: `calc(100% - ${HANDLE_SIZE / 1.5}px)`, + width: HANDLE_SIZE, + height: HANDLE_SIZE, + + borderRadius: '50%', + border: '2px solid transparent', + borderRightColor: 'rgba(100, 100, 100, 0.3)', + transform: 'rotate(45deg)', + + cursor: 'nwse-resize', +}); diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index 34fdb49079..98f484ba7d 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -1028,5 +1028,12 @@ "com.affine.page-operation.add-linked-page": "Add linked page", "com.affine.onboarding.workspace-guide.title": "Start AFFiNE by creating your own Workspace here!", "com.affine.onboarding.workspace-guide.content": "A Workspace is your virtual space to capture, create and plan as just one person or together as a team.", - "com.affine.onboarding.workspace-guide.got-it": "Got it!" + "com.affine.onboarding.workspace-guide.got-it": "Got it!", + "com.affine.calendar.weekdays.sun": "Sun", + "com.affine.calendar.weekdays.mon": "Mon", + "com.affine.calendar.weekdays.tue": "Tue", + "com.affine.calendar.weekdays.wed": "Wed", + "com.affine.calendar.weekdays.thu": "Thu", + "com.affine.calendar.weekdays.fri": "Fri", + "com.affine.calendar.weekdays.sat": "Sat" } From 5b84366de32e6913b3a9a0b2fc8b6930f34af698 Mon Sep 17 00:00:00 2001 From: LongYinan Date: Thu, 18 Jan 2024 02:37:47 +0000 Subject: [PATCH 37/92] chore: bump up actions/cache action to v4 (#5616) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [actions/cache](https://togithub.com/actions/cache) | action | major | `v3` -> `v4` | --- ### Release Notes
actions/cache (actions/cache) ### [`v4`](https://togithub.com/actions/cache/compare/v3...v4) [Compare Source](https://togithub.com/actions/cache/compare/v3...v4)
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/toeverything/AFFiNE). --- .github/actions/build-rust/action.yml | 2 +- .github/actions/setup-node/action.yml | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/actions/build-rust/action.yml b/.github/actions/build-rust/action.yml index cf396ea372..e8a24034ae 100644 --- a/.github/actions/build-rust/action.yml +++ b/.github/actions/build-rust/action.yml @@ -37,7 +37,7 @@ runs: echo "TARGET_CC=clang" >> "$GITHUB_ENV" - name: Cache cargo - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | ~/.cargo/registry/index/ diff --git a/.github/actions/setup-node/action.yml b/.github/actions/setup-node/action.yml index addc1ef70c..b2acba470d 100644 --- a/.github/actions/setup-node/action.yml +++ b/.github/actions/setup-node/action.yml @@ -63,7 +63,7 @@ runs: run: node -e "const p = $(yarn config cacheFolder --json).effective; console.log('yarn_global_cache=' + p)" >> $GITHUB_OUTPUT - name: Cache non-full yarn cache on Linux - uses: actions/cache@v3 + uses: actions/cache@v4 if: ${{ inputs.full-cache != 'true' && runner.os == 'Linux' }} with: path: | @@ -75,7 +75,7 @@ runs: # and the decompression performance on Windows is very terrible # so we reduce the number of cached files on non-Linux systems by remove node_modules from cache path. - name: Cache non-full yarn cache on non-Linux - uses: actions/cache@v3 + uses: actions/cache@v4 if: ${{ inputs.full-cache != 'true' && runner.os != 'Linux' }} with: path: | @@ -83,7 +83,7 @@ runs: key: node_modules-cache-${{ github.job }}-${{ runner.os }} - name: Cache full yarn cache on Linux - uses: actions/cache@v3 + uses: actions/cache@v4 if: ${{ inputs.full-cache == 'true' && runner.os == 'Linux' }} with: path: | @@ -92,7 +92,7 @@ runs: key: node_modules-cache-full-${{ runner.os }} - name: Cache full yarn cache on non-Linux - uses: actions/cache@v3 + uses: actions/cache@v4 if: ${{ inputs.full-cache == 'true' && runner.os != 'Linux' }} with: path: | @@ -134,7 +134,7 @@ runs: # Note: Playwright's cache directory is hard coded because that's what it # says to do in the docs. There doesn't appear to be a command that prints # it out for us. - - uses: actions/cache@v3 + - uses: actions/cache@v4 id: playwright-cache if: ${{ inputs.playwright-install == 'true' }} with: @@ -167,7 +167,7 @@ runs: run: | echo "version=$(yarn why --json electron | grep -h 'workspace:.' | jq --raw-output '.children[].locator' | sed -e 's/@playwright\/test@.*://' | head -n 1)" >> $GITHUB_OUTPUT - - uses: actions/cache@v3 + - uses: actions/cache@v4 id: electron-cache if: ${{ inputs.electron-install == 'true' }} with: From d9324286d4b3142f984ae4389b892b10d9d08705 Mon Sep 17 00:00:00 2001 From: liuyi Date: Thu, 18 Jan 2024 03:04:36 +0000 Subject: [PATCH 38/92] chore(server): add port to host if it is 0.0.0.0 (#5619) --- packages/backend/server/src/fundamentals/config/default.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/backend/server/src/fundamentals/config/default.ts b/packages/backend/server/src/fundamentals/config/default.ts index e16d3e464d..ae194f112f 100644 --- a/packages/backend/server/src/fundamentals/config/default.ts +++ b/packages/backend/server/src/fundamentals/config/default.ts @@ -92,7 +92,9 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => { return this.node.dev ? 'http://localhost:8080' : `${this.https ? 'https' : 'http'}://${this.host}${ - this.host === 'localhost' ? `:${this.port}` : '' + this.host === 'localhost' || this.host === '0.0.0.0' + ? `:${this.port}` + : '' }`; }, get baseUrl() { From f4198674371d92626a830b46ba66982799cb49d0 Mon Sep 17 00:00:00 2001 From: liuyi Date: Thu, 18 Jan 2024 03:19:20 +0000 Subject: [PATCH 39/92] chore(server): remove useless log (#5620) --- packages/backend/server/src/prelude.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/backend/server/src/prelude.ts b/packages/backend/server/src/prelude.ts index 42203d3633..f71602d779 100644 --- a/packages/backend/server/src/prelude.ts +++ b/packages/backend/server/src/prelude.ts @@ -13,10 +13,8 @@ import { const configDir = join(fileURLToPath(import.meta.url), '../config'); async function loadRemote(remoteDir: string, file: string) { - console.log(remoteDir, configDir); const filePath = join(configDir, file); if (configDir !== remoteDir) { - console.log('cp remote file'); cpSync(join(remoteDir, file), filePath, { force: true, }); From 943ede4ffd8d402ff17d6ae862ee545fe563fa47 Mon Sep 17 00:00:00 2001 From: Cats Juice Date: Thu, 18 Jan 2024 03:33:46 +0000 Subject: [PATCH 40/92] feat(component): new inline-edit component (#5517) --- packages/frontend/component/src/index.ts | 1 + .../component/src/ui/editable/index.ts | 1 + .../src/ui/editable/inline-edit.css.ts | 51 ++++ .../src/ui/editable/inline-edit.stories.tsx | 140 +++++++++++ .../component/src/ui/editable/inline-edit.tsx | 226 ++++++++++++++++++ 5 files changed, 419 insertions(+) create mode 100644 packages/frontend/component/src/ui/editable/index.ts create mode 100644 packages/frontend/component/src/ui/editable/inline-edit.css.ts create mode 100644 packages/frontend/component/src/ui/editable/inline-edit.stories.tsx create mode 100644 packages/frontend/component/src/ui/editable/inline-edit.tsx diff --git a/packages/frontend/component/src/index.ts b/packages/frontend/component/src/index.ts index d325cdb793..044b0a2d0b 100644 --- a/packages/frontend/component/src/index.ts +++ b/packages/frontend/component/src/index.ts @@ -5,6 +5,7 @@ export * from './ui/button'; export * from './ui/checkbox'; export * from './ui/date-picker'; export * from './ui/divider'; +export * from './ui/editable'; export * from './ui/empty'; export * from './ui/input'; export * from './ui/layout'; diff --git a/packages/frontend/component/src/ui/editable/index.ts b/packages/frontend/component/src/ui/editable/index.ts new file mode 100644 index 0000000000..2e6769bd51 --- /dev/null +++ b/packages/frontend/component/src/ui/editable/index.ts @@ -0,0 +1 @@ +export * from './inline-edit'; diff --git a/packages/frontend/component/src/ui/editable/inline-edit.css.ts b/packages/frontend/component/src/ui/editable/inline-edit.css.ts new file mode 100644 index 0000000000..b04f6896ed --- /dev/null +++ b/packages/frontend/component/src/ui/editable/inline-edit.css.ts @@ -0,0 +1,51 @@ +import { style } from '@vanilla-extract/css'; + +export const inlineEditWrapper = style({ + position: 'relative', + borderRadius: 4, + padding: 4, + display: 'inline-block', + minWidth: 50, + minHeight: 28, +}); +export const inlineEdit = style({ + overflow: 'hidden', + textOverflow: 'ellipsis', + + whiteSpace: 'pre', + wordWrap: 'break-word', + + // to avoid shrinking when show up + border: '1px solid transparent', + + selectors: { + [`.${inlineEditWrapper}[data-editing="true"] &`]: { + opacity: 0, + visibility: 'hidden', + }, + }, +}); + +export const inlineEditInput = style({ + position: 'absolute', + width: '100%', + height: '100%', + left: 0, + top: 0, + + opacity: 0, + visibility: 'hidden', + pointerEvents: 'none', + + selectors: { + [`.${inlineEditWrapper}[data-editing="true"] &`]: { + opacity: 1, + visibility: 'visible', + pointerEvents: 'auto', + }, + }, +}); + +export const placeholder = style({ + opacity: 0.8, +}); diff --git a/packages/frontend/component/src/ui/editable/inline-edit.stories.tsx b/packages/frontend/component/src/ui/editable/inline-edit.stories.tsx new file mode 100644 index 0000000000..b7ad44bfe0 --- /dev/null +++ b/packages/frontend/component/src/ui/editable/inline-edit.stories.tsx @@ -0,0 +1,140 @@ +import type { Meta, StoryFn } from '@storybook/react'; +import { useCallback, useRef, useState } from 'react'; + +import { Button } from '../button'; +import { ResizePanel } from '../resize-panel/resize-panel'; +import { InlineEdit, type InlineEditHandle } from './inline-edit'; + +export default { + title: 'UI/Editable/Inline Edit', + component: InlineEdit, +} satisfies Meta; + +const Template: StoryFn = args => { + const [value, setValue] = useState(args.value || ''); + return ( +
+
+
+ + Value: + + + {value} + +
+
+ + + setValue(v)} + {...args} + /> + +
+ ); +}; + +export const Basic: StoryFn = Template.bind(undefined); +Basic.args = { + editable: true, + placeholder: 'Untitled', + trigger: 'doubleClick', + autoSelect: true, +}; + +export const CustomizeText: StoryFn = + Template.bind(undefined); +CustomizeText.args = { + value: 'Customize Text', + editable: true, + placeholder: 'Untitled', + style: { + fontSize: 20, + fontWeight: 500, + padding: '10px 20px', + }, +}; + +export const TriggerEdit: StoryFn = args => { + const ref = useRef(null); + + const triggerEdit = useCallback(() => { + if (!ref.current) return; + ref.current.triggerEdit(); + }, []); + + return ( + <> +
+ +
+ + + + + ); +}; +TriggerEdit.args = { + value: 'Trigger edit mode in parent component by `handleRef`', + editable: true, + autoSelect: true, +}; + +export const UpdateValue: StoryFn = args => { + const [value, setValue] = useState(args.value || ''); + + const appendA = useCallback(() => { + setValue(v => v + 'a'); + }, []); + + return ( + <> +
+ +
+ + + + + ); +}; +UpdateValue.args = { + value: 'Update value in parent component by `value`', + editable: true, + autoSelect: true, +}; diff --git a/packages/frontend/component/src/ui/editable/inline-edit.tsx b/packages/frontend/component/src/ui/editable/inline-edit.tsx new file mode 100644 index 0000000000..845a3f4e8c --- /dev/null +++ b/packages/frontend/component/src/ui/editable/inline-edit.tsx @@ -0,0 +1,226 @@ +import clsx from 'clsx'; +import { + type CSSProperties, + type ForwardedRef, + type HTMLAttributes, + type PropsWithChildren, + useCallback, + useEffect, + useImperativeHandle, + useRef, + useState, +} from 'react'; + +import Input from '../input'; +import * as styles from './inline-edit.css'; + +export interface InlineEditHandle { + triggerEdit: () => void; +} + +export interface InlineEditProps + extends Omit, 'onChange' | 'onInput'> { + /** + * Content to be displayed + */ + value?: string; + /** + * Whether the content is editable + */ + editable?: boolean; + + onInput?: (v: string) => void; + onChange?: (v: string) => void; + + /** + * Trigger edit by `click` or `doubleClick` + * @default `'doubleClick'` + */ + trigger?: 'click' | 'doubleClick'; + + /** + * whether to auto select all text when trigger edit + */ + autoSelect?: boolean; + + /** + * Placeholder when value is empty + */ + placeholder?: string; + /** + * Custom placeholder `className` + */ + placeholderClassName?: string; + /** + * Custom placeholder `style` + */ + placeholderStyle?: CSSProperties; + + handleRef?: ForwardedRef; + + /** + * Customize attrs for the input + */ + inputAttrs?: Omit, 'onChange' | 'onBlur'>; +} + +export const InlineEdit = ({ + value, + editable, + className, + style, + trigger = 'doubleClick', + autoSelect, + + onInput, + onChange, + + placeholder, + placeholderClassName, + placeholderStyle, + + handleRef, + inputAttrs, + + ...attrs +}: InlineEditProps) => { + const [editing, setEditing] = useState(false); + const [editingValue, setEditingValue] = useState(value); + const inputRef = useRef(null); + + useImperativeHandle(handleRef, () => ({ + triggerEdit, + })); + + const triggerEdit = useCallback(() => { + if (!editable) return; + setEditing(true); + setTimeout(() => { + inputRef.current?.focus(); + autoSelect && inputRef.current?.select(); + }, 0); + }, [autoSelect, editable]); + + const onDoubleClick = useCallback(() => { + if (trigger !== 'doubleClick') return; + triggerEdit(); + }, [triggerEdit, trigger]); + + const onClick = useCallback(() => { + if (trigger !== 'click') return; + triggerEdit(); + }, [triggerEdit, trigger]); + + const submit = useCallback(() => { + onChange?.(editingValue || ''); + }, [editingValue, onChange]); + + const onEnter = useCallback(() => { + inputRef.current?.blur(); + }, []); + + const onBlur = useCallback(() => { + setEditing(false); + submit(); + // to reset input's scroll position to match actual display + inputRef.current?.scrollTo(0, 0); + }, [submit]); + + const inputHandler = useCallback( + (v: string) => { + setEditingValue(v); + onInput?.(v); + }, + [onInput] + ); + + // update editing value when value prop changes + useEffect(() => { + setEditingValue(value); + }, [value]); + + // to make sure input's style is the same as displayed text + const inputWrapperInheritsStyles = { + margin: 'inherit', + padding: 'inherit', + borderRadius: 'inherit', + fontSize: 'inherit', + fontFamily: 'inherit', + lineHeight: 'inherit', + fontWeight: 'inherit', + letterSpacing: 'inherit', + textAlign: 'inherit', + color: 'inherit', + backgroundColor: 'inherit', + } as CSSProperties; + const inputInheritsStyles = { + ...inputWrapperInheritsStyles, + padding: undefined, + margin: undefined, + }; + + return ( +
+ {/* display area, will be transparent when input */} +
+ {editingValue} + + {!editingValue && ( + + )} +
+ + {/* actual input */} + { + + } +
+ ); +}; + +interface PlaceholderProps + extends PropsWithChildren, + HTMLAttributes { + label?: string; +} +const Placeholder = ({ + label, + children, + className, + style, + ...attrs +}: PlaceholderProps) => { + return ( +
+ {children ?? label} +
+ ); +}; From aa437bcd3523142076e7568ae8228eb62e987a7e Mon Sep 17 00:00:00 2001 From: LongYinan Date: Thu, 18 Jan 2024 03:50:24 +0000 Subject: [PATCH 41/92] fix(core): remove hash prefix from cdn path (#5509) The hash prefix will cause cache invalidate during deployment It's for debug purpose but I forgot to remove it --- packages/frontend/core/.webpack/config.ts | 6 +++--- packages/frontend/core/.webpack/s3-plugin.ts | 15 +------------ .../frontend/core/.webpack/webpack.config.ts | 21 +++++++++++++++---- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/frontend/core/.webpack/config.ts b/packages/frontend/core/.webpack/config.ts index e9a92b2008..050bb6dc0f 100644 --- a/packages/frontend/core/.webpack/config.ts +++ b/packages/frontend/core/.webpack/config.ts @@ -18,7 +18,7 @@ import type { BuildFlags } from '@affine/cli/config'; import { projectRoot } from '@affine/cli/config'; import { VanillaExtractPlugin } from '@vanilla-extract/webpack-plugin'; import type { RuntimeConfig } from '@affine/env/global'; -import { WebpackS3Plugin, gitShortHash } from './s3-plugin.js'; +import { WebpackS3Plugin } from './s3-plugin.js'; const IN_CI = !!process.env.CI; @@ -81,9 +81,9 @@ export const getPublicPath = (buildFlags: BuildFlags) => { } if (BUILD_TYPE === 'canary') { - return `https://dev.affineassets.com/${gitShortHash()}/`; + return `https://dev.affineassets.com/`; } else if (BUILD_TYPE === 'beta' || BUILD_TYPE === 'stable') { - return `https://prod.affineassets.com/${gitShortHash()}/`; + return `https://prod.affineassets.com/`; } return publicPath; }; diff --git a/packages/frontend/core/.webpack/s3-plugin.ts b/packages/frontend/core/.webpack/s3-plugin.ts index 35b4b8c269..83b51a790f 100644 --- a/packages/frontend/core/.webpack/s3-plugin.ts +++ b/packages/frontend/core/.webpack/s3-plugin.ts @@ -1,24 +1,11 @@ import { join } from 'node:path'; -import { execSync } from 'node:child_process'; import { readFile } from 'node:fs/promises'; import type { PutObjectCommandInput } from '@aws-sdk/client-s3'; import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3'; -import { once } from 'lodash-es'; import { lookup } from 'mime-types'; import type { Compiler, WebpackPluginInstance } from 'webpack'; -export const gitShortHash = once(() => { - const { GITHUB_SHA } = process.env; - if (GITHUB_SHA) { - return GITHUB_SHA.substring(0, 9); - } - const sha = execSync(`git rev-parse --short HEAD`, { - encoding: 'utf-8', - }).trim(); - return sha; -}); - export const R2_BUCKET = process.env.R2_BUCKET! ?? (process.env.BUILD_TYPE === 'canary' ? 'assets-dev' : 'assets-prod'); @@ -45,7 +32,7 @@ export class WebpackS3Plugin implements WebpackPluginInstance { const putObjectCommandOptions: PutObjectCommandInput = { Body: assetSource, Bucket: R2_BUCKET, - Key: join(gitShortHash(), asset), + Key: asset, }; const contentType = lookup(asset); if (contentType) { diff --git a/packages/frontend/core/.webpack/webpack.config.ts b/packages/frontend/core/.webpack/webpack.config.ts index a9c82e0c38..17460fa873 100644 --- a/packages/frontend/core/.webpack/webpack.config.ts +++ b/packages/frontend/core/.webpack/webpack.config.ts @@ -1,14 +1,27 @@ -import { createConfiguration, rootPath } from './config.js'; -import { merge } from 'webpack-merge'; +import { execSync } from 'node:child_process'; import { join, resolve } from 'node:path'; + +import { once } from 'lodash-es'; +import { merge } from 'webpack-merge'; import type { BuildFlags } from '@affine/cli/config'; -import { getRuntimeConfig } from './runtime-config.js'; import HTMLPlugin from 'html-webpack-plugin'; -import { gitShortHash } from './s3-plugin.js'; +import { getRuntimeConfig } from './runtime-config.js'; +import { createConfiguration, rootPath } from './config.js'; const DESCRIPTION = `There can be more than Notion and Miro. AFFiNE is a next-gen knowledge base that brings planning, sorting and creating all together.`; +const gitShortHash = once(() => { + const { GITHUB_SHA } = process.env; + if (GITHUB_SHA) { + return GITHUB_SHA.substring(0, 9); + } + const sha = execSync(`git rev-parse --short HEAD`, { + encoding: 'utf-8', + }).trim(); + return sha; +}); + export default async function (cli_env: any, _: any) { const flags: BuildFlags = JSON.parse( Buffer.from(cli_env.flags, 'hex').toString('utf-8') From 74a3a795bd1e124cb43b2116dc4a21572fb10be0 Mon Sep 17 00:00:00 2001 From: EYHN Date: Thu, 18 Jan 2024 04:46:51 +0000 Subject: [PATCH 42/92] fix(workspace): check session before get workspaces (#5621) --- packages/frontend/workspace-impl/src/cloud/list.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/frontend/workspace-impl/src/cloud/list.ts b/packages/frontend/workspace-impl/src/cloud/list.ts index 26ffbed04c..03e04baf13 100644 --- a/packages/frontend/workspace-impl/src/cloud/list.ts +++ b/packages/frontend/workspace-impl/src/cloud/list.ts @@ -10,6 +10,7 @@ import { globalBlockSuiteSchema } from '@affine/workspace'; import { Workspace as BlockSuiteWorkspace } from '@blocksuite/store'; import { difference } from 'lodash-es'; import { nanoid } from 'nanoid'; +import { getSession } from 'next-auth/react'; import { applyUpdate, encodeStateAsUpdate } from 'yjs'; import { createLocalBlobStorage } from '../local/blob'; @@ -18,6 +19,10 @@ import { CLOUD_WORKSPACE_CHANGED_BROADCAST_CHANNEL_KEY } from './consts'; import { createAffineStaticStorage } from './sync'; async function getCloudWorkspaceList() { + const session = await getSession(); + if (!session) { + return []; + } try { const { workspaces } = await fetcher({ query: getWorkspacesQuery, From 9fdbb3ac3de4a64f0c2049e13ed9a886a70e364e Mon Sep 17 00:00:00 2001 From: liuyi Date: Thu, 18 Jan 2024 04:59:52 +0000 Subject: [PATCH 43/92] fix(server): should not listen on user defined host (#5622) --- packages/backend/server/src/config/affine.ts | 4 ---- packages/backend/server/src/index.ts | 5 +++-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/backend/server/src/config/affine.ts b/packages/backend/server/src/config/affine.ts index 7ae037507f..fa3a2f4e90 100644 --- a/packages/backend/server/src/config/affine.ts +++ b/packages/backend/server/src/config/affine.ts @@ -4,11 +4,7 @@ const env = process.env; const node = AFFiNE.node; // TODO(@forehalo): detail explained -AFFiNE.host = 'localhost'; -AFFiNE.port = 3010; - if (node.prod) { - AFFiNE.host = '0.0.0.0'; // Storage if (env.R2_OBJECT_STORAGE_ACCOUNT_ID) { AFFiNE.storage.providers.r2 = { diff --git a/packages/backend/server/src/index.ts b/packages/backend/server/src/index.ts index 8499eec2d4..19a01d349d 100644 --- a/packages/backend/server/src/index.ts +++ b/packages/backend/server/src/index.ts @@ -5,9 +5,10 @@ import './prelude'; import { createApp } from './app'; const app = await createApp(); -await app.listen(AFFiNE.port, AFFiNE.host); +const listeningHost = AFFiNE.deploy ? '0.0.0.0' : 'localhost'; +await app.listen(AFFiNE.port, listeningHost); console.log( - `AFFiNE Server has been started on http://${AFFiNE.host}:${AFFiNE.port}.` + `AFFiNE Server has been started on http://${listeningHost}:${AFFiNE.port}.` ); console.log(`And the public server should be recognized as ${AFFiNE.baseUrl}`); From c5ea6fd2c3c3952917cc72d9fe300a43d080f83c Mon Sep 17 00:00:00 2001 From: LongYinan Date: Thu, 18 Jan 2024 05:55:54 +0000 Subject: [PATCH 44/92] fix(server): selfhost issues (#5623) - env name in helm chart - omit health check controller in selfhost env --- .../helm/affine/charts/graphql/templates/deployment.yaml | 2 +- packages/backend/server/src/app.module.ts | 3 ++- packages/backend/server/src/fundamentals/config/def.ts | 7 +++++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/helm/affine/charts/graphql/templates/deployment.yaml b/.github/helm/affine/charts/graphql/templates/deployment.yaml index 5452a8564b..9c3e9b60ce 100644 --- a/.github/helm/affine/charts/graphql/templates/deployment.yaml +++ b/.github/helm/affine/charts/graphql/templates/deployment.yaml @@ -73,7 +73,7 @@ spec: value: "{{ .Values.app.path }}" - name: AFFINE_SERVER_HOST value: "{{ .Values.app.host }}" - - name: AFFINE_SERVER_HOST + - name: AFFINE_SERVER_HTTPS value: "{{ .Values.app.https }}" - name: ENABLE_R2_OBJECT_STORAGE value: "{{ .Values.app.objectStorage.r2.enabled }}" diff --git a/packages/backend/server/src/app.module.ts b/packages/backend/server/src/app.module.ts index 8efc1b1f32..3307d14853 100644 --- a/packages/backend/server/src/app.module.ts +++ b/packages/backend/server/src/app.module.ts @@ -33,6 +33,7 @@ export const FunctionalityModules: Array = [ }, ], imports: [...FunctionalityModules, ...BusinessModules], - controllers: [AppController], + controllers: + process.env.SERVER_FLAVOR === 'selfhosted' ? [] : [AppController], }) export class AppModule {} diff --git a/packages/backend/server/src/fundamentals/config/def.ts b/packages/backend/server/src/fundamentals/config/def.ts index ce8e80533e..55b95e1fa6 100644 --- a/packages/backend/server/src/fundamentals/config/def.ts +++ b/packages/backend/server/src/fundamentals/config/def.ts @@ -10,6 +10,13 @@ declare global { // eslint-disable-next-line no-var var AFFiNE: AFFiNEConfig; } + + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace NodeJS { + interface ProcessEnv { + SERVER_FLAVOR: ServerFlavor | ''; + } + } } export enum ExternalAccount { From a64854319edc43b1956a13dba598242c9b94b991 Mon Sep 17 00:00:00 2001 From: Cats Juice Date: Thu, 18 Jan 2024 07:04:36 +0000 Subject: [PATCH 45/92] feat(core): add document title for shared page (#5596) --- packages/frontend/core/src/hooks/use-global-state.ts | 5 +++++ packages/frontend/core/src/pages/share/share-detail-page.tsx | 3 +++ .../core/src/pages/workspace/detail-page/detail-page.tsx | 4 ++-- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/frontend/core/src/hooks/use-global-state.ts b/packages/frontend/core/src/hooks/use-global-state.ts index b42becafe9..dface9a866 100644 --- a/packages/frontend/core/src/hooks/use-global-state.ts +++ b/packages/frontend/core/src/hooks/use-global-state.ts @@ -1,3 +1,4 @@ +import type { PageMeta } from '@blocksuite/store'; import { noop } from 'lodash-es'; import { useEffect } from 'react'; @@ -14,3 +15,7 @@ export function useDocumentTitle(newTitle?: string | null) { }; }, [newTitle]); } + +export function usePageDocumentTitle(pageMeta?: PageMeta) { + useDocumentTitle(pageMeta?.title ? `${pageMeta.title} · AFFiNE` : null); +} diff --git a/packages/frontend/core/src/pages/share/share-detail-page.tsx b/packages/frontend/core/src/pages/share/share-detail-page.tsx index fa667a19db..4c1143957a 100644 --- a/packages/frontend/core/src/pages/share/share-detail-page.tsx +++ b/packages/frontend/core/src/pages/share/share-detail-page.tsx @@ -1,4 +1,5 @@ import { MainContainer } from '@affine/component/workspace'; +import { usePageDocumentTitle } from '@affine/core/hooks/use-global-state'; import { DebugLogger } from '@affine/debug'; import { fetchWithTraceReport } from '@affine/graphql'; import { globalBlockSuiteSchema } from '@affine/workspace'; @@ -119,6 +120,8 @@ export const loader: LoaderFunction = async ({ params }) => { export const Component = (): ReactElement => { const { page, publishMode } = useLoaderData() as LoaderData; + usePageDocumentTitle(page.meta); + return ( diff --git a/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx b/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx index bbc820083a..cf305f9d76 100644 --- a/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx +++ b/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx @@ -35,7 +35,7 @@ import { import { TrashPageFooter } from '../../../components/pure/trash-page-footer'; import { TopTip } from '../../../components/top-tip'; import { useRegisterBlocksuiteEditorCommands } from '../../../hooks/affine/use-register-blocksuite-editor-commands'; -import { useDocumentTitle } from '../../../hooks/use-global-state'; +import { usePageDocumentTitle } from '../../../hooks/use-global-state'; import { useNavigateHelper } from '../../../hooks/use-navigate-helper'; import { performanceRenderLogger } from '../../../shared'; import { PageNotFound } from '../../404'; @@ -115,7 +115,7 @@ const DetailPageImpl = memo(function DetailPageImpl({ page }: { page: Page }) { const mode = useAtomValue(currentModeAtom); const setPageMode = useSetAtom(setPageModeAtom); useRegisterBlocksuiteEditorCommands(currentPageId, mode); - useDocumentTitle(pageMeta?.title ? `${pageMeta.title} · AFFiNE` : null); + usePageDocumentTitle(pageMeta); const onLoad = useCallback( (page: Page, editor: AffineEditorContainer) => { From cabedef426f1b8000646fcf8e634077d5a47db36 Mon Sep 17 00:00:00 2001 From: Cats Juice Date: Thu, 18 Jan 2024 07:17:14 +0000 Subject: [PATCH 46/92] feat(core): journal hooks and page header layout (#5549) feat(core): split page header items feat(core): journal page judgment and header layout feat(core): Add journal today button and style changes to share menu --- .../affine/share-page-modal/index.tsx | 8 +- .../share-page-modal/share-menu/index.css.ts | 5 + .../share-menu/share-menu.tsx | 14 +- .../block-suite-header-title/index.tsx | 159 ------------------ .../block-suite-header-title/styles.css.ts | 43 ----- .../block-suite-header/favorite/index.tsx | 46 +++++ .../journal/date-picker.tsx | 42 +++++ .../journal/today-button.tsx | 28 +++ .../menu/index.tsx} | 98 ++++++----- .../block-suite-header/title/index.tsx | 57 +++++++ .../block-suite-header/title/style.css.ts | 6 + .../src/components/pure/header/style.css.tsx | 1 - .../frontend/core/src/hooks/use-journal.ts | 113 +++++++++++++ .../core/src/pages/share/share-header.tsx | 22 ++- .../detail-page/detail-page-header.css.ts | 9 + .../detail-page/detail-page-header.tsx | 95 +++++++++-- 16 files changed, 465 insertions(+), 281 deletions(-) delete mode 100644 packages/frontend/core/src/components/blocksuite/block-suite-header-title/index.tsx delete mode 100644 packages/frontend/core/src/components/blocksuite/block-suite-header-title/styles.css.ts create mode 100644 packages/frontend/core/src/components/blocksuite/block-suite-header/favorite/index.tsx create mode 100644 packages/frontend/core/src/components/blocksuite/block-suite-header/journal/date-picker.tsx create mode 100644 packages/frontend/core/src/components/blocksuite/block-suite-header/journal/today-button.tsx rename packages/frontend/core/src/components/blocksuite/{block-suite-header-title/operation-menu.tsx => block-suite-header/menu/index.tsx} (73%) create mode 100644 packages/frontend/core/src/components/blocksuite/block-suite-header/title/index.tsx create mode 100644 packages/frontend/core/src/components/blocksuite/block-suite-header/title/style.css.ts create mode 100644 packages/frontend/core/src/hooks/use-journal.ts diff --git a/packages/frontend/core/src/components/affine/share-page-modal/index.tsx b/packages/frontend/core/src/components/affine/share-page-modal/index.tsx index 2a7cca033e..f9b2eb1512 100644 --- a/packages/frontend/core/src/components/affine/share-page-modal/index.tsx +++ b/packages/frontend/core/src/components/affine/share-page-modal/index.tsx @@ -13,9 +13,14 @@ import { ShareMenu } from './share-menu'; type SharePageModalProps = { workspace: Workspace; page: Page; + isJournal?: boolean; }; -export const SharePageButton = ({ workspace, page }: SharePageModalProps) => { +export const SharePageButton = ({ + workspace, + page, + isJournal, +}: SharePageModalProps) => { const [open, setOpen] = useState(false); const { openPage } = useNavigateHelper(); @@ -35,6 +40,7 @@ export const SharePageButton = ({ workspace, page }: SharePageModalProps) => { return ( <> setOpen(true)} diff --git a/packages/frontend/core/src/components/affine/share-page-modal/share-menu/index.css.ts b/packages/frontend/core/src/components/affine/share-page-modal/share-menu/index.css.ts index e8d56153eb..0275804daf 100644 --- a/packages/frontend/core/src/components/affine/share-page-modal/share-menu/index.css.ts +++ b/packages/frontend/core/src/components/affine/share-page-modal/share-menu/index.css.ts @@ -157,3 +157,8 @@ globalStyle(`${shareLinkStyle} > span`, { globalStyle(`${shareLinkStyle} > div > svg`, { color: 'var(--affine-link-color)', }); + +export const journalShareButton = style({ + height: 32, + padding: '0px 8px', +}); diff --git a/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-menu.tsx b/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-menu.tsx index 2747fc8f0b..5e385c2aa7 100644 --- a/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-menu.tsx +++ b/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-menu.tsx @@ -6,6 +6,7 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks'; import type { WorkspaceMetadata } from '@affine/workspace'; import { WebIcon } from '@blocksuite/icons'; import type { Page } from '@blocksuite/store'; +import clsx from 'clsx'; import { useIsSharedPage } from '../../../../hooks/affine/use-is-shared-page'; import * as styles from './index.css'; @@ -15,6 +16,7 @@ import { SharePage } from './share-page'; export interface ShareMenuProps { workspaceMetadata: WorkspaceMetadata; currentPage: Page; + isJournal?: boolean; onEnableAffineCloud: () => void; } @@ -50,7 +52,11 @@ const LocalShareMenu = (props: ShareMenuProps) => { modal: false, }} > - @@ -76,7 +82,11 @@ const CloudShareMenu = (props: ShareMenuProps) => { modal: false, }} > - + ); +}; diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-header-title/operation-menu.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-header/menu/index.tsx similarity index 73% rename from packages/frontend/core/src/components/blocksuite/block-suite-header-title/operation-menu.tsx rename to packages/frontend/core/src/components/blocksuite/block-suite-header/menu/index.tsx index 459570096c..dfe7f6bea2 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-header-title/operation-menu.tsx +++ b/packages/frontend/core/src/components/blocksuite/block-suite-header/menu/index.tsx @@ -4,13 +4,15 @@ import { MenuItem, MenuSeparator, } from '@affine/component/ui/menu'; -import { - Export, - FavoriteTag, - MoveToTrash, -} from '@affine/core/components/page-list'; +import { currentModeAtom } from '@affine/core/atoms/mode'; +import { PageHistoryModal } from '@affine/core/components/affine/page-history-modal'; +import { Export, MoveToTrash } from '@affine/core/components/page-list'; +import { useBlockSuiteMetaHelper } from '@affine/core/hooks/affine/use-block-suite-meta-helper'; +import { useExportPage } from '@affine/core/hooks/affine/use-export-page'; +import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper'; import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta'; import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace'; +import { toast } from '@affine/core/utils'; import { WorkspaceFlavour } from '@affine/env/workspace'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { assertExists } from '@blocksuite/global/utils'; @@ -27,21 +29,21 @@ import { import { useAtomValue } from 'jotai'; import { useCallback, useState } from 'react'; -import { currentModeAtom } from '../../../atoms/mode'; -import { useBlockSuiteMetaHelper } from '../../../hooks/affine/use-block-suite-meta-helper'; -import { useExportPage } from '../../../hooks/affine/use-export-page'; -import { useTrashModalHelper } from '../../../hooks/affine/use-trash-modal-helper'; -import { toast } from '../../../utils'; -import { PageHistoryModal } from '../../affine/page-history-modal/history-modal'; -import { HeaderDropDownButton } from '../../pure/header-drop-down-button'; -import { usePageHelper } from '../block-suite-page-list/utils'; +import { HeaderDropDownButton } from '../../../pure/header-drop-down-button'; +import { usePageHelper } from '../../block-suite-page-list/utils'; +import { useFavorite } from '../favorite'; type PageMenuProps = { rename?: () => void; pageId: string; + isJournal?: boolean; }; // fixme: refactor this file -export const PageHeaderMenuButton = ({ rename, pageId }: PageMenuProps) => { +export const PageHeaderMenuButton = ({ + rename, + pageId, + isJournal, +}: PageMenuProps) => { const t = useAFFiNEI18N(); // fixme(himself65): remove these hooks ASAP @@ -54,9 +56,10 @@ export const PageHeaderMenuButton = ({ rename, pageId }: PageMenuProps) => { meta => meta.id === pageId ); const currentMode = useAtomValue(currentModeAtom); - const favorite = pageMeta?.favorite ?? false; - const { togglePageMode, toggleFavorite, duplicate } = + const { favorite, toggleFavorite } = useFavorite(pageId); + + const { togglePageMode, duplicate } = useBlockSuiteMetaHelper(blockSuiteWorkspace); const { importFile } = usePageHelper(blockSuiteWorkspace); const { setTrashModal } = useTrashModalHelper(blockSuiteWorkspace); @@ -78,14 +81,6 @@ export const PageHeaderMenuButton = ({ rename, pageId }: PageMenuProps) => { }); }, [pageId, pageMeta, setTrashModal]); - const handleFavorite = useCallback(() => { - toggleFavorite(pageId); - toast( - favorite - ? t['com.affine.toastMessage.removedFavorites']() - : t['com.affine.toastMessage.addedFavorites']() - ); - }, [favorite, pageId, t, toggleFavorite]); const handleSwitchMode = useCallback(() => { togglePageMode(pageId); toast( @@ -107,18 +102,20 @@ export const PageHeaderMenuButton = ({ rename, pageId }: PageMenuProps) => { const EditMenu = ( <> - - - - } - data-testid="editor-option-menu-rename" - onSelect={rename} - style={menuItemStyle} - > - {t['Rename']()} - + {!isJournal && ( + + + + } + data-testid="editor-option-menu-rename" + onSelect={rename} + style={menuItemStyle} + > + {t['Rename']()} + + )} @@ -136,7 +133,7 @@ export const PageHeaderMenuButton = ({ rename, pageId }: PageMenuProps) => { @@ -162,18 +159,20 @@ export const PageHeaderMenuButton = ({ rename, pageId }: PageMenuProps) => { {t['com.affine.header.option.add-tag']()} */} - - - - } - data-testid="editor-option-menu-duplicate" - onSelect={handleDuplicate} - style={menuItemStyle} - > - {t['com.affine.header.option.duplicate']()} - + {!isJournal && ( + + + + } + data-testid="editor-option-menu-duplicate" + onSelect={handleDuplicate} + style={menuItemStyle} + > + {t['com.affine.header.option.duplicate']()} + + )} @@ -232,7 +231,6 @@ export const PageHeaderMenuButton = ({ rename, pageId }: PageMenuProps) => { onOpenChange={setHistoryModalOpen} /> ) : null} - ); }; diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-header/title/index.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-header/title/index.tsx new file mode 100644 index 0000000000..b55a64b507 --- /dev/null +++ b/packages/frontend/core/src/components/blocksuite/block-suite-header/title/index.tsx @@ -0,0 +1,57 @@ +import { InlineEdit, type InlineEditProps } from '@affine/component'; +import { + useBlockSuitePageMeta, + usePageMetaHelper, +} from '@affine/core/hooks/use-block-suite-page-meta'; +import type { BlockSuiteWorkspace } from '@affine/core/shared'; +import type { HTMLAttributes } from 'react'; +import { useCallback } from 'react'; + +import * as styles from './style.css'; + +export interface BlockSuiteHeaderTitleProps { + blockSuiteWorkspace: BlockSuiteWorkspace; + pageId: string; + /** if set, title cannot be edited */ + isPublic?: boolean; + inputHandleRef?: InlineEditProps['handleRef']; +} + +const inputAttrs = { + 'data-testid': 'title-content', +} as HTMLAttributes; +export const BlocksuiteHeaderTitle = (props: BlockSuiteHeaderTitleProps) => { + const { + blockSuiteWorkspace: workspace, + pageId, + isPublic, + inputHandleRef, + } = props; + const currentPage = workspace.getPage(pageId); + const pageMeta = useBlockSuitePageMeta(workspace).find( + meta => meta.id === currentPage?.id + ); + const title = pageMeta?.title; + const { setPageTitle } = usePageMetaHelper(workspace); + + const onChange = useCallback( + (v: string) => { + setPageTitle(currentPage?.id || '', v); + }, + [currentPage?.id, setPageTitle] + ); + + return ( + + ); +}; diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-header/title/style.css.ts b/packages/frontend/core/src/components/blocksuite/block-suite-header/title/style.css.ts new file mode 100644 index 0000000000..8a3218f0af --- /dev/null +++ b/packages/frontend/core/src/components/blocksuite/block-suite-header/title/style.css.ts @@ -0,0 +1,6 @@ +import { style } from '@vanilla-extract/css'; + +export const title = style({ + fontWeight: 500, + color: 'var(--affine-text-primary-color)', +}); diff --git a/packages/frontend/core/src/components/pure/header/style.css.tsx b/packages/frontend/core/src/components/pure/header/style.css.tsx index faea9cac52..d989bc8418 100644 --- a/packages/frontend/core/src/components/pure/header/style.css.tsx +++ b/packages/frontend/core/src/components/pure/header/style.css.tsx @@ -122,5 +122,4 @@ export const headerDivider = style({ height: '20px', width: '1px', background: 'var(--affine-border-color)', - margin: '0 12px', }); diff --git a/packages/frontend/core/src/hooks/use-journal.ts b/packages/frontend/core/src/hooks/use-journal.ts new file mode 100644 index 0000000000..f864100867 --- /dev/null +++ b/packages/frontend/core/src/hooks/use-journal.ts @@ -0,0 +1,113 @@ +import type { PageMeta } from '@blocksuite/store'; +import { initEmptyPage } from '@toeverything/infra/blocksuite'; +import dayjs from 'dayjs'; +import { useCallback, useMemo } from 'react'; + +import type { BlockSuiteWorkspace } from '../shared'; +import { useBlockSuiteWorkspaceHelper } from './use-block-suite-workspace-helper'; +import { useNavigateHelper } from './use-navigate-helper'; + +type MaybeDate = Date | string | number; +export const JOURNAL_DATE_FORMAT = 'YYYY-MM-DD'; + +function isPageJournal(pageMeta?: PageMeta) { + return !!(pageMeta && pageMeta.title.match(/^\d{4}-\d{2}-\d{2}$/)); +} + +function getJournalDate(pageMeta?: PageMeta) { + if (!isPageJournal(pageMeta)) return null; + if (!pageMeta?.title) return null; + if (!dayjs(pageMeta.title).isValid()) return null; + return dayjs(pageMeta.title); +} + +export const useJournalHelper = (workspace: BlockSuiteWorkspace) => { + const bsWorkspaceHelper = useBlockSuiteWorkspaceHelper(workspace); + const navigateHelper = useNavigateHelper(); + + /** + * @internal + */ + const _createJournal = useCallback( + (maybeDate: MaybeDate) => { + const title = dayjs(maybeDate).format(JOURNAL_DATE_FORMAT); + const page = bsWorkspaceHelper.createPage(); + initEmptyPage(page, title).catch(err => + console.error('Failed to load journal page', err) + ); + return page; + }, + [bsWorkspaceHelper] + ); + + /** + * query all journals by date + */ + const getJournalsByDate = useCallback( + (maybeDate: MaybeDate) => { + const day = dayjs(maybeDate); + return Array.from(workspace.pages.values()).filter(page => { + if (!isPageJournal(page.meta)) return false; + if (page.meta.trash) return false; + const journalDate = getJournalDate(page.meta); + if (!journalDate) return false; + return day.isSame(journalDate, 'day'); + }); + }, + [workspace.pages] + ); + + /** + * get journal by date, create one if not exist + */ + const getJournalByDate = useCallback( + (maybeDate: MaybeDate) => { + const pages = getJournalsByDate(maybeDate); + if (pages.length) return pages[0]; + return _createJournal(maybeDate); + }, + [_createJournal, getJournalsByDate] + ); + + /** + * open journal by date, create one if not exist + */ + const openJournal = useCallback( + (maybeDate: MaybeDate) => { + const page = getJournalByDate(maybeDate); + navigateHelper.openPage(workspace.id, page.id); + }, + [getJournalByDate, navigateHelper, workspace.id] + ); + + /** + * open today's journal + */ + const openToday = useCallback(() => { + const date = dayjs().format(JOURNAL_DATE_FORMAT); + openJournal(date); + }, [openJournal]); + + return useMemo( + () => ({ + getJournalsByDate, + getJournalByDate, + openJournal, + openToday, + }), + [getJournalByDate, getJournalsByDate, openJournal, openToday] + ); +}; + +export const useJournalInfoHelper = (pageMeta?: PageMeta) => { + const isJournal = isPageJournal(pageMeta); + const journalDate = useMemo(() => getJournalDate(pageMeta), [pageMeta]); + + return useMemo( + () => ({ + isJournal, + journalDate, + }), + [isJournal, journalDate] + ); +}; diff --git a/packages/frontend/core/src/pages/share/share-header.tsx b/packages/frontend/core/src/pages/share/share-header.tsx index 3cf7f9ef78..9ca199f1ae 100644 --- a/packages/frontend/core/src/pages/share/share-header.tsx +++ b/packages/frontend/core/src/pages/share/share-header.tsx @@ -1,7 +1,8 @@ +import { EditorModeSwitch } from '@affine/core/components/blocksuite/block-suite-mode-switch'; import type { Workspace as BlockSuiteWorkspace } from '@blocksuite/store'; import type { PageMode } from '../../atoms'; -import { BlockSuiteHeaderTitle } from '../../components/blocksuite/block-suite-header-title'; +import { BlocksuiteHeaderTitle } from '../../components/blocksuite/block-suite-header/title/index'; import ShareHeaderLeftItem from '../../components/cloud/share-header-left-item'; import ShareHeaderRightItem from '../../components/cloud/share-header-right-item'; import { Header } from '../../components/pure/header'; @@ -20,12 +21,19 @@ export function ShareHeader({ isFloat={publishMode === 'edgeless'} left={} center={ - + <> + + + } right={ -
+
{showSidebarSwitch ? : null}
@@ -100,27 +103,71 @@ function Header({ ); } -export function DetailPageHeader({ - page, - workspace, - showSidebarSwitch = true, -}: { +interface PageHeaderProps { page: Page; workspace: Workspace; showSidebarSwitch?: boolean; -}) { +} +const RightHeader = isWindowsDesktop + ? WindowsMainPageHeaderRight + : NonWindowsMainPageHeaderRight; +export function JournalPageHeader({ + page, + workspace, + showSidebarSwitch = true, +}: PageHeaderProps) { const leftSidebarOpen = useAtomValue(appSidebarOpenAtom); - const RightHeader = isWindowsDesktop - ? WindowsMainPageHeaderRight - : NonWindowsMainPageHeaderRight; return (
{!leftSidebarOpen ? : null} - +
+ +
+ + + + {page ? ( + + ) : null} + +
+ ); +} + +export function NormalPageHeader({ + page, + workspace, + showSidebarSwitch = true, +}: PageHeaderProps) { + const titleInputHandleRef = useRef(null); + const leftSidebarOpen = useAtomValue(appSidebarOpenAtom); + + const onRename = useCallback(() => { + setTimeout(() => titleInputHandleRef.current?.triggerEdit()); + }, []); + return ( +
+ + {!leftSidebarOpen ? : null} + + + +
{page ? : null} @@ -128,6 +175,18 @@ export function DetailPageHeader({ ); } +export function DetailPageHeader(props: PageHeaderProps) { + const { page } = props; + const { isJournal } = useJournalInfoHelper(page.meta); + const isInTrash = page.meta.trash; + + return isJournal && !isInTrash ? ( + + ) : ( + + ); +} + function WindowsSidebarHeader() { return ( <> From 9a944048e8154b72e19831cf484e96ac5ca607ae Mon Sep 17 00:00:00 2001 From: Peng Xiao Date: Thu, 18 Jan 2024 07:53:15 +0000 Subject: [PATCH 47/92] feat(core): experimental features ui (#5338) fix TOV-280 experimental features ui - enabled in the workspace settings for a cloud workspace; only show for workspace owner + early access - a disclaimer prompt will be shown before going to the next feature setting page - for now only show the ai poc switch, which controls the display of the ai tab in editor's sidepanel --- packages/common/env/src/global.ts | 1 - .../component/src/ui/switch/switch.tsx | 3 +- .../frontend/core/.webpack/runtime-config.ts | 8 - .../setting-modal/general-setting/index.tsx | 12 +- .../components/affine/setting-modal/index.tsx | 54 +- .../setting-modal/setting-sidebar/index.tsx | 217 +++-- .../setting-sidebar/style.css.ts | 27 +- .../affine/setting-modal/style.css.ts | 1 + .../components/affine/setting-modal/types.ts | 21 + .../experimental-features/arts.css.ts | 7 + .../experimental-features/arts.tsx | 18 + .../experimental-features/dark-art-svg.ts | 857 ++++++++++++++++++ .../experimental-features/index.css.ts | 64 ++ .../experimental-features/index.tsx | 149 +++ .../experimental-features/light-art-svg.ts | 843 +++++++++++++++++ .../setting-modal/workspace-setting/index.tsx | 21 +- .../workspace-list/index.tsx | 2 +- .../core/src/hooks/use-workspace-features.ts | 74 ++ .../detail-page/editor-sidebar/atoms.ts | 7 +- .../editor-sidebar/extensions/extensions.tsx | 17 +- .../frontend/graphql/src/graphql/index.ts | 13 + .../graphql/workspace-enabled-features.gql | 5 + packages/frontend/graphql/src/schema.ts | 14 + packages/frontend/i18n/src/resources/en.json | 8 + 24 files changed, 2297 insertions(+), 146 deletions(-) create mode 100644 packages/frontend/core/src/components/affine/setting-modal/types.ts create mode 100644 packages/frontend/core/src/components/affine/setting-modal/workspace-setting/experimental-features/arts.css.ts create mode 100644 packages/frontend/core/src/components/affine/setting-modal/workspace-setting/experimental-features/arts.tsx create mode 100644 packages/frontend/core/src/components/affine/setting-modal/workspace-setting/experimental-features/dark-art-svg.ts create mode 100644 packages/frontend/core/src/components/affine/setting-modal/workspace-setting/experimental-features/index.css.ts create mode 100644 packages/frontend/core/src/components/affine/setting-modal/workspace-setting/experimental-features/index.tsx create mode 100644 packages/frontend/core/src/components/affine/setting-modal/workspace-setting/experimental-features/light-art-svg.ts create mode 100644 packages/frontend/core/src/hooks/use-workspace-features.ts create mode 100644 packages/frontend/graphql/src/graphql/workspace-enabled-features.gql diff --git a/packages/common/env/src/global.ts b/packages/common/env/src/global.ts index 7e9527be4e..40fbf74ee7 100644 --- a/packages/common/env/src/global.ts +++ b/packages/common/env/src/global.ts @@ -29,7 +29,6 @@ export const runtimeFlagsSchema = z.object({ enableEnhanceShareMode: z.boolean(), enablePayment: z.boolean(), enablePageHistory: z.boolean(), - enableCopilot: z.boolean(), // this is for the electron app serverUrlPrefix: z.string(), enableMoveDatabase: z.boolean(), diff --git a/packages/frontend/component/src/ui/switch/switch.tsx b/packages/frontend/component/src/ui/switch/switch.tsx index b26c896396..739d47e47f 100644 --- a/packages/frontend/component/src/ui/switch/switch.tsx +++ b/packages/frontend/component/src/ui/switch/switch.tsx @@ -19,6 +19,7 @@ export const Switch = ({ checked: checkedProp = false, onChange: onChangeProp, children, + className, ...otherProps }: SwitchProps) => { const [checkedState, setCheckedState] = useState(checkedProp); @@ -34,7 +35,7 @@ export const Switch = ({ ); return ( -